프로메테우스 수출상인 게으른 개발 방식을 쓰다

본문은 C# Advent Series의 일부분이다.크리스마스는 우리 마음속에 특별한 위치를 차지하는데, 이 활동도 지역 사회를 구축하는 데 도움을 주는 가장 좋은 방법이다.다른 작가들의 멋진 내용을 꼭 보세요!
남반구의 크리스마스는 매년 우리에게 많은 영향을 미친다. 우선, 지금은 여름이고 바깥은 매우 덥다.둘째, 거의 모든 기업의 일반적인 폐쇄이다.그러나 일부 기업은 여전히 경영을 계속하고 있다.
우리의 한 고객은 암호화폐 채굴에 종사하는데, 그들은 휴가를 보내고 가족과 함께 시간을 보내기를 원하는 직원들에게 매우 관심을 가지고 있다.그들의 유일한 직원은 GPU로 이 설비들은 연중무휴로 일할 수 있다.그러나 온도가 높아지면 효율에 영향을 받는다.또 다른 슬픈 일이 일어날 수도 있다.

솔루션 설계


우리의 첫 번째 건의는 우리의trustyELK+G를 사용하고 NVIDIA SMI 도구에서 추가 데이터를 조사하는 것이다. 그러나 우리는 이 문제가 이미 우리를 위해 해결되었다는 것을 곧 발견했다.현재 채광 소프트웨어는 매우 복잡하고 혼란스러워졌다. 채광 소프트웨어는 현재 자신의 웹 서버와 API 를 갖추고 있다.그래서 우리는 조금 간소화했다.
우리가 여기서 해야 할 일은 수출 업체를 일으켜 몇 개의 계기판을 설치하는 것이다.쉬웠어
관리형 서비스
우리는 기본적으로 두 가지 서비스를 실행해야 한다. 그것은 바로 밑바닥 API를 돌아가며 플로메테우스의 우호적인 형식으로 도량을 공개하는 것이다.우리는 .NET Core Generic host 인프라 시설이 이곳에 매우 적합하다고 생각한다.응용 프로그램을 안내하고 관리 서비스를 추가하며 Docker에 파이프라인을 남겨 둘 수 있습니다.이 프로젝트는 결국 이렇게 보입니다.
class Program
    {
        private static async Task Main(string[] args)
        {
            using IHost host = CreatHostBuilder(args).Build();

            await host.RunAsync();
        }

        static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((configuration) =>
                {

                    configuration.AddEnvironmentVariables("TREX")
; // can add more sources such as command line
                })
                .ConfigureServices(c =>
                {
                    c.AddSingleton<MetricCollection>(); // This is where we will keep all metrics state. hence singleton
                    c.AddHostedService<PrometheusExporter>(); // exposes MetricCollection
                    c.AddHostedService<TRexPoller>(); // periodically GETs status and updates MetricCollection
                });
    }

서비스 정의


우리 응용 프로그램의 두 부분은 TRexPollerPrometheusExporter이다.이 두 가지 코드를 작성하는 것은 모두 매우 간단하다. 우리는 코드에 너무 많은 시간을 쓰지 않을 것이다.GitHub에서 원하는 방식으로 액세스할 수 있습니다check it out.여기서 지적하고자 하는 것은 업무 논리에 관심을 가지고 힘든 일을 각자의 NuGet 패키지에 남기는 것이 결코 쉬운 일이 아니라는 것이다.

모형을 제작하다


우리의 응용에서 가장 중요한 부분은 당연히 원격 측정이다.API에서 json 응답 예제를 가져와 온라인 도구를 사용하여 C# 클래스로 변환했습니다.
// generated code looks like this. A set of POCOs with each property decorated with JsonProperty that maps to api response
public partial class Gpu
{
    [JsonProperty("device_id")]
    public int DeviceId { get; set; }

    [JsonProperty("hashrate")]
    public int Hashrate { get; set; }

    [JsonProperty("hashrate_day")]
    public int HashrateDay { get; set; }

    [JsonProperty("hashrate_hour")]
    public int HashrateHour { get; set; }
...
}
현재 우리는 Prometheus.Net 이후에 발견하고 제공할 수 있는 지표를 정의해야 한다.
// example taken from https://github.com/prometheus-net/prometheus-net#quick-start
private static readonly Counter ProcessedJobCount = Metrics
    .CreateCounter("myapp_jobs_processed_total", "Number of processed jobs.");

...

ProcessJob();
ProcessedJobCount.Inc();

게으름 모드 열기


이것이 바로 우리가 '저코드' 해결 방안의 계발을 받은 부분이다. 우리는 API 서비스의 모든 값을 설명하기 위해 한 무더기의 필드를 수동으로 만들고 싶지 않다.다행히도 C#9은 우리에게 새로운 기능을 제공했다. Source Code Generators 우리를 구해줘!우리는 이전covered the basic setup이 있었기 때문에 이 부분을 건너뛰고 크리스마스 마법 부분을 계속할 것이다.

코드 생성기가 우리를 위해 이 일을 하도록 하다


우리가 모든 것을 로봇에게 맡기기 전에, 우리는 이 과정을 통제하기 위해 몇 가지 기본 규칙을 설정해야 한다.사용자 정의 속성은 모든 구성이 모델 POCO에서 로컬로 유지되도록 하는 합리적인 방법으로 보입니다.
[AddInstrumentation("gpus")] // the first attribute prompts the generator to loop through the properties and search for metrics 
public partial class Gpu
{
    [JsonProperty("device_id")]
    public int DeviceId { get; set; }

    [JsonProperty("hashrate")]
    /*
     * the second attribute controls which type the metric will have as well as what labels we want to store with it.
     * In this example, it's a Gauge with gpu_id, vendor and name being labels for grouping in Prometheus
     */
    [Metric("Gauge", "gpu_id", "vendor", "name")]
    public int Hashrate { get; set; }

    [JsonProperty("hashrate_day")]
    [Metric("Gauge", "gpu_id", "vendor", "name")]
    public int HashrateDay { get; set; }

    [JsonProperty("hashrate_hour")]
    [Metric("Gauge", "gpu_id", "vendor", "name")]
    public int HashrateHour { get; set; }

마지막으로 생성기 자체가 ClassDeclarationSyntax에 연결되어 알려진 속성을 찾습니다.
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        if (syntaxNode is ClassDeclarationSyntax cds && cds.AttributeLists
            .SelectMany(al => al.Attributes)
            .Any(a => (a.Name as IdentifierNameSyntax)?.Identifier.ValueText == "AddInstrumentation"))
        {
            ClassesToProcess.Add(cds);
        }
    }

목록을 얻은 후, 우리는 모든 속성을 반복해서 Collector 개 대상의 사전을 생성합니다.
var text = new StringBuilder(@"public static Dictionary<string, Collector> GetMetrics(string prefix)
    {
        var result = new Dictionary<string, Collector>
        {").AppendLine();
foreach (PropertyDeclarationSyntax p in properties)
{
    var jsonPropertyAttr = p.GetAttr("JsonProperty");
    var metricAttr = p.GetAttr("Metric");

    if (metricAttr == null) continue;

    var propName = jsonPropertyAttr.GetFirstParameterValue();
    var metricName = metricAttr.GetFirstParameterValue(); // determine metric type
    if (metricAttr.ArgumentList.Arguments.Count > 1)
    {
        var labels = metricAttr.GetTailParameterValues(); // if we have extra labels to process - here's our chance 
        text.AppendLine(
            $"{{$\"{{prefix}}{attrPrefix}_{propName}\", Metrics.Create{metricName}($\"{{prefix}}{attrPrefix}_{propName}\", \"{propName}\", {commonLabels}, {labels}) }},");
    }
    else
    {
        text.AppendLine(
            $"{{$\"{{prefix}}{attrPrefix}_{propName}\", Metrics.Create{metricName}($\"{{prefix}}{attrPrefix}_{propName}\", \"{propName}\", {commonLabels}) }},");
    }
}
text.AppendLine(@"};
                return result;
            }");
메트릭에 대한 스토리지를 정의하는 동시에 API 피드백을 받는 즉시 값을 업데이트할 코드를 생성해야 합니다.
private StringBuilder UpdateMetrics(List<MemberDeclarationSyntax> properties, SyntaxToken classToProcess, string attrPrefix)
{
    var text = new StringBuilder($"public static void UpdateMetrics(string prefix, Dictionary<string, Collector> metrics, {classToProcess} data, string host, string slot, string algo, List<string> extraLabels = null) {{");
    text.AppendLine();
    text.AppendLine(@"if(extraLabels == null) { 
                            extraLabels = new List<string> {host, slot, algo};
                        }
                        else {
                            extraLabels.Insert(0, algo);
                            extraLabels.Insert(0, slot);
                            extraLabels.Insert(0, host);
                        }");

    foreach (PropertyDeclarationSyntax p in properties)
    {
        var jsonPropertyAttr = p.GetAttr("JsonProperty");
        var metricAttr = p.GetAttr("Metric");

        if (metricAttr == null) continue;

        var propName = jsonPropertyAttr.GetFirstParameterValue();
        var metricName = metricAttr.GetFirstParameterValue();
        var newValue = $"data.{p.Identifier.ValueText}";

        text.Append(
            $"(metrics[$\"{{prefix}}{attrPrefix}_{propName}\"] as {metricName}).WithLabels(extraLabels.ToArray())");
        switch (metricName)
        {
            case "Counter": text.AppendLine($".IncTo({newValue});"); break;
            case "Gauge": text.AppendLine($".Set({newValue});"); break;
        }
    }

    text.AppendLine("}").AppendLine();
    return text;
}

MetricCollection과 결합


마지막으로 생성된 코드를 사용하여 각 모델에서 메트릭을 부트하고 업데이트가 올바르게 처리되는지 확인할 수 있습니다.
internal class MetricCollection
{
    private readonly Dictionary<string, Collector> _metrics;
    private readonly string _prefix;
    private readonly string _host;

    public MetricCollection(IConfiguration configuration)
    {
        _prefix = configuration.GetValue<string>("exporterPrefix", "trex");
        _metrics = new Dictionary<string, Collector>();
        // this is where declaring particl classes and generating extra methods makes for seamless development experience
        foreach (var (key, value) in TRexResponse.GetMetrics(_prefix)) _metrics.Add(key, value);
        foreach (var (key, value) in DualStat.GetMetrics(_prefix)) _metrics.Add(key, value);
        foreach (var (key, value) in Gpu.GetMetrics(_prefix)) _metrics.Add(key, value);
        foreach (var (key, value) in Shares.GetMetrics(_prefix)) _metrics.Add(key, value);
    }

    public void Update(TRexResponse data)
    {
        TRexResponse.UpdateMetrics(_prefix, _metrics, data, _host, "main", data.Algorithm);
        DualStat.UpdateMetrics(_prefix, _metrics, data.DualStat, _host, "dual", data.DualStat.Algorithm);

        foreach (var dataGpu in data.Gpus)
        {
            Gpu.UpdateMetrics(_prefix, _metrics, dataGpu, _host, "main", data.Algorithm, new List<string>
            {
                dataGpu.DeviceId.ToString(),
                dataGpu.Vendor,
                dataGpu.Name
            });
            Shares.UpdateMetrics(_prefix, _metrics, dataGpu.Shares, _host, "main", data.Algorithm, new List<string>
            {
                dataGpu.GpuId.ToString(),
                dataGpu.Vendor,
                dataGpu.Name
            });
        }
    }
}

생성된 코드를 정탐하다


우리가 정확한 궤도에 있는지 확인하기 위해서, 우리는 생성된 코드를 보았다.예쁘지는 않지만 성실한 일입니다.
public partial class Shares {

public static Dictionary<string, Collector> GetMetrics(string prefix)
                {
                    var result = new Dictionary<string, Collector>
                    {
{$"{prefix}_shares_accepted_count", Metrics.CreateCounter($"{prefix}_shares_accepted_count", "accepted_count", "host", "slot", "algo", "gpu_id", "vendor", "name") },
{$"{prefix}_shares_invalid_count", Metrics.CreateCounter($"{prefix}_shares_invalid_count", "invalid_count", "host", "slot", "algo", "gpu_id", "vendor", "name") },
{$"{prefix}_shares_last_share_diff", Metrics.CreateGauge($"{prefix}_shares_last_share_diff", "last_share_diff", "host", "slot", "algo", "gpu_id", "vendor", "name") },
...
};
                            return result;
                        }

public static void UpdateMetrics(string prefix, Dictionary<string, Collector> metrics, Shares data, string host, string slot, string algo, List<string> extraLabels = null) {
if(extraLabels == null) { 
                                    extraLabels = new List<string> {host, slot, algo};
                                }
                                else {
                                    extraLabels.Insert(0, algo);
                                    extraLabels.Insert(0, slot);
                                    extraLabels.Insert(0, host);
                                }
(metrics[$"{prefix}_shares_accepted_count"] as Counter).WithLabels(extraLabels.ToArray()).IncTo(data.AcceptedCount);
(metrics[$"{prefix}_shares_invalid_count"] as Counter).WithLabels(extraLabels.ToArray()).IncTo(data.InvalidCount);
(metrics[$"{prefix}_shares_last_share_diff"] as Gauge).WithLabels(extraLabels.ToArray()).Set(data.LastShareDiff);

...
}
}

결론


이 예는 이 기능의 표면에 거의 닿지 않았다.우리가 단조롭고 중복된 개발 작업을 처리할 때, 원본 코드 생성기는 매우 유용하다.그것은 또한 우리가 성명식 방법으로 전환해서 유지 보수 비용을 줄이는 데 도움을 준다.나는 우리가 더 많은 프로젝트가 곧 이 기능에 나타날 것이라고 믿는다.
만약 아직 없다면 반드시 체크아웃the source code in GitHub해야 한다.우리에게 있어서, 이 명절을 끝낼 때, 우리는 삼가 가장 열렬한 안부를 드리며, 모두들 새해 복 많이 받으세요.

좋은 웹페이지 즐겨찾기