En este post vamos a introducir el manejo a bajo nivel que hace SQL de las tareas que le piden los usuarios, como algunos sabréis SQL tiene su proprio “sistema operativo” que se encarga del scheduling, memoria etcetera, este componente recibe el original nombre de SQLOS. En este post nos centraremos en la parte de scheduling, para apoyar las explicaciones vamos a usar las vistas dinámicas que proporciona SQL Server a partir de su versión 2005 (http://msdn.microsoft.com/en-us/library/ms188754.aspx).

Nodes

Los nodos pertenecen más a la parte de memoria, pero juegan un papel muy importante a la hora de crear otros los schedulers, SQLOS crea un nodo por cada nodo NUMA (http://en.wikipedia.org/wiki/Non-Uniform_Memory_Access) que tenga la máquina más un nodo adicional para la conexión dedicada de administración (DAC), si nuestra máquina es UMA (http://en.wikipedia.org/wiki/Uniform_Memory_Access) se crearan dos nodos uno para uso normal y otro para el DAC.

Para ver los nodos que ha creado nuestro SQLOS podemos recurrir a la vista: sys.dm_os_nodes (http://msdn.microsoft.com/en-us/library/bb510628(v=sql.100).aspx), en mi máquina podemos ver que solo crea dos nodos, uno para uso normal y otro para uso del DAC.

image

El id de los nodos es consecutivo excepto el DAC que para 32bits es 32 y para 64bits es 64. En el estado podemos ver como todos están ONLINE (normalmente deben estar así), luego su dirección de memoria del nodo que para tareas de depuración de SQLOS viene muy bien y la máscara de afinidad del nodo, en sistemas con un solo socket veremos que la afinidad es 255, en sistemas con varios sockets veremos que la afinidad se ajusta al numero de cores de cada socket.

Para procesadores X64 indicar que todos los procesadores con controladora interna de memoria son procesadores NUMA esto incluye todos los procesadores AMD (Opteron, Athlon X64 y posteriores) y los procesadores de intel basados en Nehalem (http://en.wikipedia.org/wiki/Nehalem_(microarchitecture)) y posteriores (Sandy Bridge, Ivy Bridge…)

La tecnología IA64 (Itanium) es NUMA.

Schedulers

Para estudiar los diferentes componentes vamos a ir de abajo a arriba, en la parte más baja entran los schedulers, este componente es el encargado de realizar el scheduling entre las diferentes tareas tanto de usuario como de sistema. SQL Server crea un scheduler de usuario por cada CPU lógica que tenga el sistema, y algunos más para tareas internas.

Para ver los schedulers del sistema tenemos la vista dinámica sys.dm_os_schedulers (http://msdn.microsoft.com/en-us/library/ms177526.aspx). La salida de esta vista en mi máquina:

image

Como con los nodos tenemos la dirección de memoria del Scheduler así como a que nodo pertenece, mi máquina es un i7 (Nehalem) de un solo socket y cuatro cores, como tengo Hyper-Threading (HT) activado el número de cores lógicos es ocho, por tanto SQLOS crea ocho schedulers, uno por core lógico, para tareas de usuario (status = VISIBLE), adicionalmente crea 4 schedulers para tareas administrativas y  uno para el DAC. La columna is_idle nos indica si el scheduler está ejecutando algo [0] o está sin hacer nada [1], Failed_to_create_worker es una columna interesante ya que nos indica si el scheduler a fallado en crear un worker [1] en algun momento, si es así podemos encontrarnos ante problemas de presión de memoria, o un valor de worker threads (http://msdn.microsoft.com/en-us/library/ms187024.aspx) muy alto si hemos establecido esta opción a mano, no recomendable.

Indicar que en máquinas dedicadas exclusivamente a SQL puede ser buena opción desactivar Hyper-Threading a nivel de BIOS esto provocará que el número de cores lógicos corresponda con el número de cores físico haciendo que SQL cree un scheduler por core físico, reduciendo posibles esperas causadas por Hyper-Threading. Para determinar el impacto en el rendimiento se recomienda probar una carga de uso normal en un entorno de preproducción tanto con HT activado como con HT desactivado.

Workers

Cada scheduler tiene un número n de workers, el número global de workers en toda la instancia no puede sobrepasar el valor ‘max worker threads’. Cuando llega un nuevo trabajo el scheduler busca un worker libre si no existe ninguno intenta crear uno nuevo y asignarle la tarea. Para poder ver los workers del sistema podemos usar la vista: sys.dm_os_workers (http://msdn.microsoft.com/en-us/library/ms178626.aspx)

image

Como siempre la primera columna (worker_address) es la dirección de memoria del objeto, is_preemtive lo veremos más adelante, exception_num es el último numero de error que se ha producido en ese worker, en la captura podemos ver un 1222 que significa lock timeout. en state podemos ver si el worker está corriendo o suspendido, en last_wait_type podemos ver por que estaba esperando en la última espera que hizo el thread, posteriormente tenemos las direcciones de memoria de: task que está ejecutando el worker, el thread de windows usado para ejecutar la tarea y el scheduler al cual pertenece este thread.

Task

Esto representa una tarea, o subtarea de un usuario, cuando el engine decide paralelizar una query creará subtasks que se asignaran a workers libres para completar la query original. La vista que nos proporciona esta información es: sys.dm_os_tasks (http://msdn.microsoft.com/en-us/library/ms174963.aspx). En mi máquina

image

Aquí podemos ver la dirección de la tarea, el estado en el que está la tarea, el scheduler en el que está ejecutando la tarea, el número de sesión de usuario que ha lanzado la tarea y la dirección del worker que está ejecutando la tarea.

Mezclandolo todo

Para empezar vamos a recordar las relaciones entre elementos.

SQLOS tiene tantos nodos como nodos NUMA tenga la máquina,

SQLOS tiene tantos schedulers como cores lógicos reporte windows.

Cada scheduler tiene n workers y un worker solo puede pertenecer a un scheduler.

Cada worker tiene uno y solo un thread.

Cada worker tiene una y solo una tarea. Una vez completada la tarea el worker puede tener una tarea nueva.

Una tarea solo puede pertenecer a un worker.

Llega una nueva tarea, ¿ahora que hago?

Cuando llega una nueva tarea se le asigna un scheduler, este scheduler busca entre sus workers si hay algun que no esté haciendo nada, si es así se asigna la tarea al worker y se coloca en la lista de workers que pueden ser ejecutado. Si no hay ningun worker libre se intenta crear uno nuevo. Cuando al worker le toque ejecutar se le dara el procesador y es el worker el que debe decidir cuanto tiempo permanece en el. ¡Multitarea colaborativa!

Multitarea colaborativa en SQLOS

Como hemos visto es decisión del worker cuando devuelve el control al scheduler para que pueda ejecutar otro worker. Todos las tareas internas de SQL Server se controlan para no estar mucho tiempo ocupando el scheduler y que la ejecución sea fluida, esto se hace por rendimiento ya que simplifica enormemente el scheduler.

Bien, pero y si una tarea tarda mucho

Para estos casos existe una monitorización de los schedulers, si un scheduler no ha hecho un cambio de contexto en cierto tiempo se genera un volcado de memoria del scheduler afectado, elimina la task y marca el worker como libre.

Seguramente muchos de vosotros os habréis encontrado con un volcado cuya descripción es “Non-Yielding Scheduler”, este tipo de volcados significa que un worker no ha retornado el control al scheduler en un tiempo adecuado, por tanto la tarea es terminada para no afectar a las restantes.

Que pasa cuando es necesario hacer una llamada a Windows.

Como muchos sabréis la multitarea de Windows es preemtiva, esto quiere decir que es el sistema operativo el que quita la ejecución a los procesos y se los asigna a otros procesos sin que el proceso afectado se entere de nada. Que pasa si tengo que hacer una llamada a Windows, ODBC, … cuya multitarea es preemtiva. En ese caso se marca el flag is_preemtive en el worker para evitar que el monitor de schedulers genere un volcado y mate el proceso en caso de que este tarde mucho en responder, como puede ser un acceso a disco, registro....

Pablo Gavela López – Microsoft Customer Support Services