Como se mencionó anteriormente, la habilidad de una arquitectura para escalar se evalúa en términos del rendimiento que puede ofrecer. Considérese la gráfica de la figura I-1 en la que se compara el rendimiento contra los recursos hardware para un diseño hipotético.
Obsérvese que la gráfica anterior muestra un crecimiento no lineal del rendimiento. En otras palabras, la tasa a la que el rendimiento crece, va disminuyendo y eventualmente se estabiliza. Tal estabilización representa el rendimiento máximo posible que la arquitectura proveerá sin importar cuántos recursos de hardware se pongan a su disposición.
El factor que determina cuán alto llegará el rendimiento es el grado de contención sobre los recursos que necesita la aplicación en tiempo de ejecución. La contención surge cuando múltiples operaciones concurrentes intentan acceder de manera exclusiva un recurso compartido. Puesto que solo una a la vez puede obtenerlo, todas las demás se bloquean y tienen que esperar hasta que el recurso esté disponible o hasta que decidan abortar o se les fuerce a hacerlo. Conforme la contención se incrementa, los cuellos de botella se crean y crecen. En estas condiciones, un incremento en la concurrencia no ofrecerá ningún beneficio adicional; de hecho, es probable que empeore las cosas. Por otra parte, es posible que una operación que espera por un recurso haya adquirido y bloqueado recursos que otras operaciones necesitan para terminar su trabajo. Los efectos que todo lo anterior produce son prolongados tiempos de respuesta y deadlocks. Una vez que este proceso comienza, las cosas solo pueden empeorar.
Para reducir la contención hay que seguir dos pasos. Primero es necesario asegurarse que los recursos se adquieren justo en el momento que se necesitan, se usen solamente el tiempo necesario y se liberen tan pronto como sea posible. En otras palabras, el código debe ser lo más eficiente posible. Desafortunadamente, trabajar solo en la eficiencia del código simplemente pospone el momento en el que el acceso a un recurso se convierte en un cuello de botella. El código más eficiente del planeta no ayudará si la concurrencia continúa en ascenso. Eventualmente, el acceso múltiple a un recurso hará necesario el segundo paso. El recurso tiene que replicarse.
El hardware es un recurso en contención. Los servidores necesitan acceder recursos hardware (ciclos de CPU, la red, discos, etc.) para procesar los requerimientos de los clientes. Esto provoca contención en los recursos hardware y por ende causará cuellos de botella. Una vez que esto ocurre, aceptar más requerimientos es un error. Siempre quedan las opciones de actualizar los recursos de hardware (CPUs, enlaces y discos más rápidos, etc.) y optimizar el código pero, como se dijo anteriormente, solo se pospone el punto en el cual se desarrolla un cuello de botella. Una vez que las opciones de actualización se agotan o resultan ser muy caras y el código es tan eficiente como es posible, el siguiente paso es agregar un servidor.
La información también es un recurso en contención. La mayoría de los requerimientos que provienen de los clientes necesitan acceder algún tipo de dato. En muchos sistemas distribuidos, cada dato se mantiene en un lugar, esto es, en un proceso o servidor particular en el que puede ser manipulado a través de alguna forma de comunicación entre procesos. Puesto que un dato en particular reside físicamente en una máquina, todas las consideraciones acerca del uso eficiente de sus recursos hardware son relevantes. Pero hay otra consideración; el acceso exclusivo al mismo dato que realicen múltiples operaciones concurrentes debe sincronizarse a través de un mecanismo de bloqueo (lock). Esto último, como es de suponer, causará contención y cuellos de botella. Dependiendo de las operaciones que se realicen y la sofisticación de los mecanismos de bloqueo, esto puede ocurrir antes de que la contención por los recursos hardware se vuelva un problema. Si se da por sentado que el código es tan eficiente como puede ser, el paso final es replicar el dato.
Desafortunadamente, la réplica de datos no es sencilla. Cuando los datos no cambian (son solamente de lectura), no es un problema; se pueden copiar tantas veces y a tantos lugares como sea necesario. En contraste, el problema se presenta cuando los datos cambian. Su réplica hace necesario contar con un mecanismo para que todas las copias estén sincronizadas lo que incrementa significativamente la complejidad del diseño. Todo lo relativo a la eficiencia y las probabilidades de falla asociadas a un ambiente distribuido hacen imposible mantener las múltiples copias sincronizadas todo el tiempo. En lugar de esto, las copias se sincronizan solo periódicamente lo que ocasiona latencia en la propagación de los cambios de una copia a otra. Si los datos replicados se usan para responder a los requerimientos de los clientes, la respuesta que un cliente obtenga depende de qué copia del dato se usó para responderle. El que la réplica de datos sea necesaria o aceptable depende del tipo de sistema que se construya.
En este momento resulta claro que el grado de contención en información y recursos hardware compartidos limita la escalabilidad de una arquitectura. La meta de un diseño escalable es eliminar la contención pero esto es imposible. En todo sistema hay un cuello de botella que, cuando se encuentra, causa que el rendimiento se estabilice tal como se mostró en la figura I-1. El rendimiento mejorará conforme cada cuello de botella se elimine pero siempre habrá un obstáculo que no permitirá que el rendimiento continúe creciendo linealmente. El número de cuellos de botella que se tiene que eliminar en un sistema depende de cuánto rendimiento se necesite. Eliminar de manera justa y suficiente los cuellos de botella es la esencia del diseño de un sistema escalable.
Diseñar un sistema escalable -tomar las decisiones correctas acerca del manejo de recursos- es difícil. Más aún, diseñar un sistema escalable que usa objetos es especialmente retador. Muchos diseños tradicionales orientados-a-objetos que se hicieron populares en el desarrollo de aplicaciones contenidas en un solo proceso, se han llevado a aplicaciones distribuidas. Desafortunadamente la tendencia de estos diseños en los que se asocia cada entidad abstracta en el dominio de un problema a un objeto en memoria inhibe la escalabilidad. La raíz del problema es que un objeto, cuando se usa como un recurso compartido, representa una fuente de contención. Para entender porqué, se requiere un entendimiento cabal del concepto de identidad. Éste es el primer paso hacia un modelo de objetos que soporte la escalabilidad y que es la meta de un sistema basado en Microsoft .NET.