Licenciamiento por núcleo para software de servidor .NET: contar los núcleos correctamente y vincular el derecho
El licenciamiento por núcleo vive o muere según se cuenten bien los núcleos. Aquí tienes por qué Environment.ProcessorCount engaña, cómo leer los núcleos físicos y las cuotas de CPU de un contenedor en .NET, y cómo vincular un derecho de recuento de núcleos a una licencia firmada.
El licenciamiento por núcleo es como se vende mucho software de servidor: un precio que escala con la capacidad de CPU sobre la que te ejecuta el cliente, porque un motor de base de datos o un servicio de analítica en una máquina de 64 núcleos entrega mucho más valor que el mismo binario en una VM de 4. El modelo es simple de enunciar y sorprendentemente fácil de equivocar, porque la parte difícil no es la política: es responder, de forma correcta y resistente a la manipulación, a la pregunta “¿sobre cuántos núcleos se está ejecutando esto en realidad?”. En .NET, la API obvia te da la respuesta equivocada más a menudo que la correcta.
La trampa del recuento
Echa mano de Environment.ProcessorCount y distribuirás un bug de licenciamiento. Devuelve el número de procesadores lógicos disponibles para el proceso actual, que no es la capacidad que estás vendiendo, al menos de tres maneras:
- Cuenta hyperthreads, así que un chip de 8 núcleos físicos normalmente reporta 16.
- Se encoge bajo afinidad: si el proceso está fijado a un subconjunto de CPUs (mediante máscaras de afinidad o un planificador), reporta el subconjunto, no la máquina.
- Ignora las cuotas del contenedor: en un pod de Kubernetes limitado a 2 CPUs en un nodo de 64 núcleos, puede seguir reportando un número grande según el runtime y la versión; no estás midiendo el límite que el orquestador impuso realmente.
Así que antes de escribir una sola comprobación, decide qué número es tu unidad de licenciamiento: núcleos físicos, procesadores lógicos o la cuota de CPU del contenedor. Luego mide ese a propósito.
Medir el número correcto
Procesadores lógicos (barato, pero lo menos significativo por sí solo):
int logical = Environment.ProcessorCount;
Núcleos físicos en Windows — suma NumberOfCores a través de los sockets:
using System.Management; // Windows only
int physical = 0;
using var searcher = new ManagementObjectSearcher("SELECT NumberOfCores FROM Win32_Processor");
foreach (var mo in searcher.Get())
physical += Convert.ToInt32(mo["NumberOfCores"]);
Núcleos físicos en Linux — cuenta pares únicos (physical id, core id):
var pairs = new HashSet<(string socket, string core)>();
string socket = "0", core = "0";
foreach (var line in File.ReadLines("/proc/cpuinfo"))
{
if (line.StartsWith("physical id")) socket = line.Split(':')[1].Trim();
else if (line.StartsWith("core id")) core = line.Split(':')[1].Trim();
else if (line.Length == 0) pairs.Add((socket, core));
}
int physicalLinux = Math.Max(pairs.Count, 1);
Cuota de CPU del contenedor — el número que de verdad importa en Kubernetes/Docker:
// cgroup v2: /sys/fs/cgroup/cpu.max → "<quota> <period>" or "max <period>"
static double? CgroupCpuLimit()
{
const string v2 = "/sys/fs/cgroup/cpu.max";
if (File.Exists(v2))
{
var parts = File.ReadAllText(v2).Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts[0] == "max") return null; // unlimited
return double.Parse(parts[0]) / double.Parse(parts[1]);
}
// cgroup v1
const string q = "/sys/fs/cgroup/cpu/cpu.cfs_quota_us";
const string p = "/sys/fs/cgroup/cpu/cpu.cfs_period_us";
if (File.Exists(q) && File.Exists(p))
{
double quota = double.Parse(File.ReadAllText(q).Trim());
if (quota <= 0) return null; // unlimited
return quota / double.Parse(File.ReadAllText(p).Trim());
}
return null;
}
Elige uno de estos como tu capacidad medida: la mayoría de los productos de servidor se decantan por los núcleos físicos para bare-metal/VM y por la cuota del cgroup (redondeada hacia arriba) para los contenedores.
Vincular el derecho a la licencia
Medir es solo la mitad del trabajo; el número medido tiene que compararse contra algo que el cliente no pueda editar. Ese algo es un derecho firmado. La licencia porta un valor maxCores en su payload, firmado con tu clave privada, y el cliente verifica la firma antes de confiar en él (la mecánica está en derechos de licencia y feature flags y en la anatomía de una clave firmada). Entonces, al arranque:
var license = keyright.Validate(); // verifies signature against embedded public key
int licensedCores = license.GetInt("maxCores"); // trusted entitlement from the signed payload
int measured = MeasureLicensingCores(); // one of the numbers above, chosen per policy
if (measured > licensedCores)
{
// Fail closed — but loudly and actionably.
throw new LicenseException(
$"This license permits {licensedCores} cores; this host exposes {measured}. " +
$"Reduce the CPU allocation or upgrade the license.");
}
Dos notas de diseño que valen la tinta. Primera, falla en cerrado, pero diagnostica: un operador que programó por accidente tu servicio en un nodo más grande debería leer exactamente qué pasó —medido frente a licenciado— y no ver morir el proceso con un error genérico. Segunda, considera una gracia breve o una advertencia única antes de que la aplicación muerda, para que una sobre-asignación transitoria durante un despliegue no tumbe producción; esto refleja el razonamiento sobre gracia de caídas del servidor de licencias y ventanas de gracia. Como el recuento de núcleos se lee localmente y el derecho se verifica localmente, toda la comprobación funciona sin conexión y en cada arranque sin necesidad de llamar a casa.
Hacer esto con Keyright
Keyright te permite poner un derecho maxCores (o maxLogicalProcessors, o una cuota de contenedor) en la licencia firmada, y su SDK de .NET verifica ese payload contra una clave pública que embebes en tiempo de compilación, de modo que la comparación entre núcleos licenciados y medidos se ejecuta localmente, sin conexión y fallando en cerrado, con el mensaje de diagnóstico que elijas. Tú decides la política de recuento; Keyright te da el número resistente a la manipulación con el que compararla. Es autoalojado, así que tu clave de firma y los datos de licencia de tus clientes permanecen en infraestructura que tú controlas.
Consulta los planes, o parte de añadir licenciamiento a una app .NET y añade el derecho de recuento de núcleos al modelo que ya tienes.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.