字体选项

Posts

Crane 推荐组件源码笔记

选自我的笔记,项目非常庞大,希望给各位同好一点启发,这里主要还是推荐部分模块的源码走读

架构

The project structure is organized into several directories and files, each serving a specific purpose. Here’s a brief overview of the main components

  • cmd/
    • metric-adapter/: Contains code related to the metric adapter component
    • craned/: Contains the main application code for the craned command
      • app/: Likely contains application-specific logic and configurations
      • main.go: The entry point for the craned application
    • crane-agent/: Contains code related to the crane agent component
  • deploy/: Contains deployment scripts and configurations for deploying the application
  • docs/: Documentation files for the project
  • examples/: Example configurations or usage examples for the project
  • hack/: Scripts and utilities for development and maintenance tasks
  • overrides/: Possibly contains configuration overrides or custom settings
  • pkg/: Contains the core library code, which can be used by other applications or commands
  • site/: Could be related to the project’s website or documentation site
  • tools/: Additional tools or scripts that support the project
  • Files:
    • go.mod and go.sum: Go module files that manage dependencies
    • README.md and README_zh.md: Documentation files providing an overview and instructions for the project
    • Makefile: Contains build instructions and tasks for the project
    • Dockerfile: Instructions for building a Docker image of the application
    • .gitignore: Specifies files and directories to be ignored by Git
    • .golangci.yaml: Configuration for GolangCI, a Go linter
    • CONTRIBUTING.md and CODE_OF_CONDUCT.md: Guidelines for contributing to the project and expected behavior

Manager 部分

  1. Command Initialization:
    • NewManagerCommand(ctx context.Context) *cobra.Command:
      • Creates a new Cobra command for craned
      • Initializes options using options.NewOptions()
      • Sets up command flags and feature gates
  2. Command Execution:
    • The Run function within the Cobra command is executed:
      • Calls opts.Complete() to complete option setup
      • Validates options with opts.Validate()
      • Executes Run(ctx, opts) to start the manager
  3. Run Function:
    • Run(ctx context.Context, opts *options.Options) error:
      • Configures the controller manager with ctrl.GetConfigOrDie()
      • Sets up controller options, including leader election and metrics binding
      • Creates a new manager with ctrl.NewManager(config, ctrlOptions)
  4. Health Checks:
    • Adds health and readiness checks using mgr.AddHealthzCheck and mgr.AddReadyzCheck
  5. Data Sources and Predictor Initialization:
    • initDataSources(mgr, opts): Initializes real-time and historical data sources
    • initPredictorManager(opts, realtimeDataSources, historyDataSources): Sets up the predictor manager using the initialized data sources
  6. Scheme and Indexer Initialization:
    • initScheme(): Registers API types with the runtime scheme
    • initFieldIndexer(mgr): Sets up field indexers for efficient querying
  7. Webhooks Initialization:
    • initWebhooks(mgr, opts): Configures webhooks if necessary
  8. Pod OOM Recorder:
    • Initializes oom.PodOOMRecorder to track out-of-memory events
    • Sets up the recorder with the manager and runs it in a separate goroutine
  9. Controller Initialization:
    • initControllers(ctx, podOOMRecorder, mgr, opts, predictorMgr, historyDataSources[providers.PrometheusDataSource]): Initializes various controllers, including analytics and recommendation controllers
  10. Metric Collector Initialization:
  • initMetricCollector(mgr): Sets up custom metrics collection
  1. Running All Components:
  • runAll(ctx, mgr, predictorMgr, dataSourceProviders[providers.PrometheusDataSource], opts):
    • Starts all components, ensuring that the manager and predictor manager are running

Controllers 部分

PodOOMRecorder

  1. SetupWithManager:
    • SetupWithManager(mgr ctrl.Manager) error:
      • This function is called to register the PodOOMRecorder with the controller manager
      • It sets up the reconciler to watch for pod events and trigger the Reconcile function
  2. Reconcile Function:
    • Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error):
      • Triggered by the controller manager for each pod event
      • Retrieves the pod specified in the request
      • Checks if the pod was terminated due to an OOM event using IsOOMKilled(pod)
      • If OOMKilled, iterates over container statuses to find the terminated container
      • For each OOMKilled container, checks if memory requests are set
      • Increments the OOM count metric and adds an OOMRecord to the queue
  3. Run Function:
    • Run(stopCh <-chan struct{}) error:
      • Continuously processes the OOM records from the queue
      • Uses a ticker to periodically clean old OOM records to prevent exceeding ConfigMap storage limits
      • Retrieves OOM records using GetOOMRecord
      • Updates OOM records with updateOOMRecord
  4. GetOOMRecord Function:
    • GetOOMRecord() ([]OOMRecord, error):
      • Retrieves OOM records from the cache or ConfigMap
      • If the cache is nil, it fetches the ConfigMap and unmarshals the OOM records
  5. cleanOOMRecords Function:
    • cleanOOMRecords(oomRecords []OOMRecord) []OOMRecord:
      • Cleans up old OOM records to maintain the maximum number specified by OOMRecordMaxNumber
  6. updateOOMRecord Function:
    • updateOOMRecord(oomRecord OOMRecord, saved []OOMRecord) error:
      • Updates the OOM records in the cache or storage with new records
  7. IsOOMKilled Function:
    • IsOOMKilled(pod *v1.Pod) bool:
      • Helper function to determine if a pod was terminated due to an OOM event

Detailed Flow

  • Initialization: The PodOOMRecorder is set up with the manager using SetupWithManager, which registers it to watch for pod events
  • Event Handling: For each pod event, Reconcile is called to check for OOM events and record them
  • Record Management: The Run function processes the queue of OOM records, periodically cleans old records, and updates the storage
  • Data Retrieval: GetOOMRecord fetches the current OOM records, either from the cache or the ConfigMap

This detailed explanation provides a comprehensive view of the PodOOMRecorder’s functionality and its role in tracking OOM events in a Kubernetes cluster

阅读更多

一个小思路 - 我是如何实现K8s资源操作备份的

本篇文章使用了类似 “回收站” 的方式,对于被删除的资源进行了一个备份操作,使得出现误操作或者想找回的时候,有效减少恢复时间

相关的项目代码已经开源至 Github,当前支持 K8s 1.24+ 版本

思路

整体手段其实非常简单,我们使用了 Informer 的机制,通过 Watch 到相关资源的方式,当触发 “Delete” 操作的时候,将 YAML 正确抓取出来并上传到存储空间即可,具体流程可以查看下面的流程图

+------------------+     +------------------+     +------------------+
|                  |     |                  |     |                  |
|  配置加载 (init) |---->| 创建 K8s 客户端  |---->|  创建 S3 客户端  |
|                  |     |                  |     |                  |
+------------------+     +------------------+     +------------------+
                                  |
                                  v
+------------------+     +------------------+     +------------------+
|                  |     |                  |     |                  |
|   监听终止信号   |<----| 启动资源监控器   |---->| 创建工作队列处理 |
|                  |     |                  |     |                  |
+------------------+     +------------------+     +------------------+
        |                        |                        |
        |                        v                        v
        |               +------------------+     +------------------+
        |               |                  |     |                  |
        |               | 监听资源删除事件 |---->|  将资源放入队列  |
        |               |                  |     |                  |
        |               +------------------+     +------------------+
        |                                                |
        |                                                v
        |                                      +------------------+
        |                                      |                  |
        |                                      | 从队列获取资源   |
        |                                      |                  |
        |                                      +------------------+
        |                                                |
        |                                                v
        |                                      +------------------+
        |                                      |                  |
        |                                      |  上传到 S3 存储  |
        |                                      |                  |
        |                                      +------------------+
        |
        v
+------------------+
|                  |
|    程序退出      |
|                  |
+------------------+
  1. 程序启动时加载配置
  2. 创建 Kubernetes 客户端连接
  3. 创建 S3 存储客户端连接
  4. 启动资源监控器,监听资源删除事件
  5. 当资源被删除时,将资源信息放入工作队列
  6. 工作队列处理器从队列中获取资源信息
  7. 将资源 YAML 上传到 S3 存储
  8. 程序接收到终止信号时优雅退出

理论存在,实践特别容易,就不需要展开赘述,这里分享一个小技巧,在进行 K8s 编程的时候也能经常用到

阅读更多

MindSearch 本地部署和简单体验

简介

最近发现到了 MindSearch 这个新兴项目。根据官方的介绍,MindSearch 是一个开源的AI搜索引擎框架。截至 2024 年 9 月 19 日,它仍处于初创阶段,仅具备 Demo 级别的功能,但其背后的技术和设计理念却引起了我的兴趣,它试图通过模拟人类的思维过程,从而提供更为深入和精准的信息检索体验。但是目前网络极少关于这个项目部署和介绍的文档,这也算变相推广介绍了。

PS: 项目目前迭代非常快,因此本文以参考为主

原理

MindSearch 是一款开源的AI搜索引擎,旨在通过模拟人类思维过程来提供深度的信息检索能力。其技术原理主要基于一个多智能体框架(Multi-Agent),结合了大语言模型和复杂的搜索策略,以实现高效的信息获取和处理。受限于项目的快速发展,这里主要以介绍逻辑而不是具体的代码实现(同时也是基于项目论文进行总结)

参考论文:《MindSearch: Mimicking Human Minds Elicits Deep AI Searcher》

核心组件(逻辑)

WebPlanner

WebPlanner 是 MindSearch 中的高级规划器,负责将用户的查询分解为多个子问题。其工作流程包括:

  • 查询分解:当用户输入问题时,WebPlanner 会分析该问题,并将其拆分为多个可以独立解决的子问题。这种分解使得复杂问题能够被逐步处理。
  • 动态图构建:WebPlanner 利用有向无环图(DAG)来动态构建问题解决的逻辑图。每个子问题作为图中的节点,随着信息的获取,图会不断扩展和细化。这种方法模拟了人类在解决问题时的思维过程,使得搜索结果更加符合逻辑。

构建完成的 DAG 包含了多个节点和边,其中节点表示独立的搜索任务,边表示节点之间的推理关系。每个节点代表一个独立的搜索任务,包括一个 START 节点(用户的原始问题)和一个 END 节点(最终的回答)

WebSearcher

WebSearcher 是负责实际信息检索的组件,其功能包括:

  • 并行搜索:对于每个从 WebPlanner 接收到的子问题,WebSearcher 会进行并行搜索。这意味着多个子问题可以同时被处理,从而显著提高信息检索的速度。
  • 层次化信息检索:WebSearcher 采用从粗到细的检索策略,首先广泛收集相关信息,然后针对最有价值的页面进行深入分析。这种策略确保了回答的全面性和准确性。
  • 信息提取:在获取到相关网页后,WebSearcher 会提取与子问题相关的关键信息,并将其汇总回WebPlanner,以便生成最终答案。

相较于市面上目前的商业相似品,例如 Perplexity.ai 或者 GPT Research,它更多强调去中心化和用户控制,同时并不是简单的将索引到的网页当成上下文传入到 LLM 进行辅助检索,缺点也正是因为每个步骤都会逐步分解,导致不具备立刻即时响应的能力,以及如果使用商业大模型会非常消耗 Token(如果是 GPT-4 恐怕分分钟烧光余额),一些简单的问题就不适应使用这种方式进行检索了

image.png

部署

推荐使用 2C2G 的机器进行部署,磁盘推荐大于 20G 以留出空间给 Python 依赖,同时出于搜索速度也推荐使用境外服务器进行部署,这里优先选择直接安装,或者自己手动封装成镜像,整个项目分为前后端分离

阅读更多

尝试使用手动部署 Kubernetes 集群

本文更新于 2024-12-26,Kubernetes 更新非常快,请注意可能有过时信息

前言

最近公司业务场景要一个高可用的集群节点用于工业环境,而且数据量非常大,刚刚好手上有一台 IDC 淘来的机架服务器,借机复习一下 K8s 的手动部署。

系统资源

虚拟机配置

$ free -h
               total        used        free      shared  buff/cache   available
Mem:            31Gi       901Mi        25Gi       4.0Mi       5.4Gi        30Gi
Swap:             0B          0B          0B
$ cat /proc/cpuinfo | grep -c processor
32

集群的安装

由于只安装一个 Control Panel,我们只需要配置好以下几个组件即可

  • ETCD
  • Kube-ApiServer
  • Kube-Controller-Manager
  • Kube-Scheduler
  • Kube-Proxy
  • Kubelet

基础的配置

安装基础软件

$ sudo apt install bash-completion git net-tools sudo build-essential golang conntrack
$ go version # 这里的 Go 版本需要大于 1.12
go version go1.22.2 linux/amd64

进行组件的编译

阅读更多

一个Pod建立的背后 - 从Kubernetes源码角度剖析

前言

Pod 是 Kubernetes 集群中的基础单元,它们可以组织和运行多个容器。从用户的角度,Pod 可以很方便地被创建和管理,但是它们的实际工作原理却鲜有人关注或者一知半解。在这篇文章中,我们将从 Kubernetes 源码的角度,详细剖析一个 Pod 建立的背后。同时会分析各个组件的工作流程,以及它们如何协同工作,以实现创建和管理 Pod 的目标。同时我们还将在代码解读中,讲解每个组件的职责和实现原理,以更好地理解 Pod 工作的核心机制。

Pod基础知识

Pod 由一个或多个 Container 组成,或者可以说是容器的抽象,所有容器共享一个网络命名空间、存储卷等,可以很方便地共享数据和通信。

首先我们需要通过声明式配置文件来创建 Pod,描述容器的配置和所需的资源,并通过 kubectl 命令提交给 Kubernetes 集群中,而后集群的调度器也就是 Scheduler 组件会根据预先配置的亲和性与对硬件资源的考量,将 Pod 放到一个合适的节点,一旦调度成功,Kubernetes 会启动其中的容器并为其分配网络命名空间、IP 地址和存储卷。

在 Pod 启动成功后,Kubernetes 会不断监视其状态,并提供了更新、扩展和故障转移等编排功能,而在这篇文章中,我将会假设您已经有了对于 Kubernetes 已经具备了一定程度的水平和经验。同时,为了保证学习的流畅性,文章会摒弃例如 PriorityClass 之类的对于主流程关联性没有那么重要的特性

创建Pod的过程

我们在上面基础复习的时候,已经简述了一个 Pod 从组件上是如何被传递并且被创建了,但是如果想要阅读源码,不仅需要查看代码实现,还需要理解 Kubernetes 的架构设计和组件间的通信机制。

过程简述

我们首先将 YAML 声明使用 kubectl 进行提交,kubectl 会通过 Kubeconfig 文件与 kube-apiserver 进行通信。kubeconfig 中的 token 会与 kube-apiserver 进行校验,这个 token 通常被配置在集群中某一个 Secrets 中,与某个具备一定权限的 ServiceAccount 做了绑定,在通过认证后 kube-apiserver 将对提交的 YAML 声明进行语法和逻辑检查,以确保其符合 Kubernetes 的要求。这些检查包括验证声明中的资源是否存在、依赖关系是否正确等,如果没有问题则将其写到 Etcd 中。

阅读更多

通过RBAC权限最小化创建云效AppStack流水线用户

云效官方文档对于 Pipeline 的权限要求

权限要求

权限类型非分批发布分批发布
Namespace读权限读权限
ControllerRevision (apps/v1/ControllerRevision)全部权限全部权限
待部署的 Kubernetes 对象全部权限全部权限
Rollout (standard.oam.dev/v1alpha1)-全部权限
待部署的工作负载对象全部权限全部权限
CRD (apiextensions.k8s.io/v1/CustomResourceDefinition)-全部权限(用于安装 Rollout CRD)
ServiceAccount (core/v1/ServiceAccount)-全部权限(用于维持 Rollout 控制器的运行)
ClusterRole (rbac.authorization.k8s.io/v1/ClusterRole)-全部权限(用于维持 Rollout 控制器的运行)
ClusterRoleBinding (rbac.authorization.k8s.io/v1/ClusterRoleBinding)-全部权限(用于维持 Rollout 控制器的运行)
Role (rbac.authorization.k8s.io/v1/Role)-全部权限(用于维持 Rollout 控制器的运行)
RoleBinding (rbac.authorization.k8s.io/v1/RoleBinding)-全部权限(用于维持 Rollout 控制器的运行)
Deployment (apps/v1/Deployment)-全部权限(用于维持 Rollout 控制器的运行)
Pod (core/v1/Pod)-全部权限(用于 Rollout 控制器的安装后 E2E 测试)

注:"-" 表示在该场景下不需要相应的权限。

阅读更多

Ruby实现一个简单的DNS查询工具

前言

最近需要写一个自动轮询生成指定域名的最快 IP 的工具,本身是一个小工具的属性,直接是用 Ruby 调试加上编写花费了三个小时完成了部分功能,过程比我想象地要难一点,这里直接放出来代码给大家参考

同时我也参考了这段 Gist :https://gist.github.com/jvns/1e5838a53520e45969687e2f90199770

效果

Wireshark效果截图

代码和部分讲解

# frozen_string_literal: true

require 'socket'
require 'stringio'

DNS_TYPES = {
  1 => "A",
  2 => "NS",
  5 => "CNAME",
}

class DNSEncapsulationOfWriteOperations
  # fixed format, just hardcode
  private def make_question_header(query_id)
    [query_id, 0x0100, 0x0001, 0x0000, 0x0000, 0x0000].pack('nnnnnn')
  end

  public def make_dns_query(domain, query_id)
    question = domain.split('.').map { |label| [label.length, label].pack('Ca*') }.join + "\x00"
    question << [0x0001, 0x0001].pack('nn') # query type and class
    make_question_header(query_id) + question
  end
end

class DNSEncapsulationOfReadOperations
  def read_domain_name(buf)
    domain = []

    loop do
      label_length = buf.read(1).unpack1('C')
      break if label_length == 0

      if (label_length & 0xc0) == 0xc0
        # DNS compression
        pointer_offset = ((label_length & 0x3f) << 8) + buf.read(1).unpack1('C')
        old_pos = buf.pos
        buf.pos = pointer_offset
        domain << read_domain_name(buf)
        buf.pos = old_pos
        break
      else
        # Normal label
        domain << buf.read(label_length)
      end
    end

    domain.join('.')
  end

  def read_rdata(buf, length)
    case DNS_TYPES[@type] || @type
    when "CNAME", "NS"
      read_domain_name(buf)
    when "A"
      buf.read(4).unpack1('C4')
    else
      buf.read(length)
    end
  end

end

class DNSHeader
  attr_reader :id, :flags, :num_questions, :num_answers, :num_auth, :num_additional

  def initialize(buf)
    hdr = buf.read(12)
    @id, @flags, @num_questions, @num_answers, @num_auth, @num_additional = hdr.unpack('nnnnnn')
  end
end

class DNSRecord
  attr_reader :name, :type, :class, :ttl, :rdlength, :rdata, :parsed_rdata

  def initialize(buf)
    @dns_read_ops = DNSEncapsulationOfReadOperations.new
    @name = @dns_read_ops.read_domain_name(buf)
    @type, @class, @ttl, @rdlength = buf.read(10).unpack('nnNn')
    @parsed_rdata = read_rdata(buf, @rdlength)
  end

  def read_rdata(buf, length)
    @type_name = DNS_TYPES[@type] || @type
    if @type_name == "CNAME" or @type_name == "NS"
      @dns_read_ops.read_domain_name(buf)
    elsif @type_name == "A"
      buf.read(length).unpack('C*').join('.')
    else
      buf.read(length)
    end
  end

  def to_s
    "#{@name}\t\t#{@ttl}\t#{@type_name}\t#{@parsed_rdata}"
  end
end

class DNSQuery
  attr_reader :domain, :type, :cls

  def initialize(buf)
    dns_read_ops = DNSEncapsulationOfReadOperations.new
    @domain = dns_read_ops.read_domain_name(buf)
    @type, @cls = buf.read(4).unpack('nn')
  end
end

class DNSResponse
  attr_reader :header, :queries, :answers, :authorities, :additionals

  def initialize(bytes)
    buf = StringIO.new(bytes)  # 将字节数组打包成字符串并创建 StringIO 对象
    @header = DNSHeader.new(buf)
    @queries = (1..@header.num_questions).map { DNSQuery.new(buf) } if @header.num_questions > 0
    @answers = (1..@header.num_answers).map { DNSRecord.new(buf) } if @header.num_answers > 0
    @authorities = (1..@header.num_auth).map { DNSRecord.new(buf) } if @header.num_auth > 0
    @additionals = (1..@header.num_additional).map { DNSRecord.new(buf) } if @header.num_additional > 0
  end
end

def send_dns_query(sock, domain, query_id)
  dns_write_ops = DNSEncapsulationOfWriteOperations.new
  sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [3, 0].pack('l_2')) # timeout - 3s
  sock.send(dns_write_ops.make_dns_query(domain, query_id), 0)
end

def receive_dns_response(sock)
  reply, _ = sock.recvfrom(1024)
  DNSResponse.new(reply)
end

module Resolvfast
  class Error < StandardError; end

  def self.main(domain)
    begin
      sock = UDPSocket.new
      sock.bind('0.0.0.0', rand(10240..65535))
      sock.connect('223.5.5.5', 53)

      # send query
      send_dns_query(sock, domain, 1)

      # receive & parse response
      response = receive_dns_response(sock)
      print "Answers for #{domain}:\n"
      response.answers.each do |record|
        puts record
      end
    ensure
      sock.close
    end
  end
end

if ARGV.empty?
  puts "Usage: #{$PROGRAM_NAME} <domain>"
  exit 1
end

Resolvfast.main(ARGV[0])

测试

阅读更多

源码随笔 - Kured插件

前言

Kured(Kubernetes Reboot Daemon)是一个针对 Kubernetes 的守护程序,其功能是在底层操作系统的软件包管理系统指示需要重新启动时,进行安全的自动节点重启

该守护程序通过监视重启标志文件(例如 /var/run/reboot-required)或监测特定的哨兵命令是否成功运行来确定是否需要进行重启操作。它会持续不断地观察这些指示,并在需要时采取相应的行动

为了确保在进行节点重启时的平稳运行,Kured 利用了 Kubernetes 的 APIServer 中的锁机制,以保证一次只有一个节点进行重启。这种锁机制确保了在集群中同一时间只有一个节点被重启,并避免了重启操作之间的冲突

此外,Kured 还提供了一些可选功能,例如在存在活动的 Prometheus 警报或特定的 Pod 时,可以推迟执行重启操作。这样可以避免在系统处于故障状态或关键服务正在运行时进行重启,从而确保整个集群的稳定性和可用性

总体来说,这个项目整体难度不算太高,通过对这个项目的二开可以加深对于集群的理解,具体的代码可以参考 https://github.com/kubereboot/kured ,截止目前,最新版本为 1.15.1

Kured 代码流程简述

主流程

  1. 它的 flags 使用了 cobra 库,同时在 Run 形参里面声明了 Root 函数为主要的执行函数,注意不要被 Execute 的调用所疑惑了

  2. window, err := timewindow.New(rebootDays, rebootStart, rebootEnd, timezone) 这里使用了从 flags 中声明的重启时间,其中的 rebootDays 根据上下文来看,是个 []string 类型,推测估计是直接填写日期字符串

  3. buildHostCommand(1, restartCommand) 这个方法实际上是将外部的宿主机的控制权限映射到了内部的容器控制点(当然必须事先声明 hostPID:trueprivileged:true这两个选项允许容器进程获取额外的权限),如果我们进入函数内部进行查看,可以看到它是使用了 Linux 的 Nsenter 命令,这个命令会进入宿主机的命名空间(特别是在这里指定了 -m 选项,与之和宿主机的 PID 为 1 的进程做挂载)。这样,随后追加的 command 就会在宿主机上执行,而不是在容器内部

    func buildHostCommand(pid int, command []string) []string {
    
        // From the container, we nsenter into the proper PID to run the hostCommand.
        // For this, kured daemonset need to be configured with hostPID:true and privileged:true
        cmd := []string{"/usr/bin/nsenter", fmt.Sprintf("-m/proc/%d/ns/mnt", pid), "--"}
        cmd = append(cmd, command...)
        return cmd
    }
    
  4. 而在 Root 函数中,剩余的代码作用于设置和触发节点重启的逻辑,代码这里展示了两种重启的策略方法

阅读更多

从Brutal算法的实现引申出的随想

前篇

关于 TCP 拥塞的控制算法,入门知识可以从 《TCP拥塞控制算法BBR的原理和改进实践》开始过一遍

Brutal 算法的核心代码剖析

static void brutal_update_rate(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct brutal *brutal = inet_csk_ca(sk);

    u64 sec = tcp_sock_get_sec(tp);
    u64 min_sec = sec - PKT_INFO_SLOTS;
    u32 acked = 0, losses = 0;
    u32 ack_rate; // Scaled by 100 (100=1.00) as kernel doesn't support float
    u64 rate = brutal->rate;
    u32 cwnd;

    u32 mss = tp->mss_cache;
    u32 rtt_ms = (tp->srtt_us >> 3) / USEC_PER_MSEC;
    if (!rtt_ms)
        rtt_ms = 1;

    for (int i = 0; i < PKT_INFO_SLOTS; i++)
    {
        if (brutal->slots[i].sec >= min_sec)
        {
            acked += brutal->slots[i].acked;
            losses += brutal->slots[i].losses;
        }
    }
    if (acked + losses < MIN_PKT_INFO_SAMPLES)
        ack_rate = 100;
    else
    {
        ack_rate = acked * 100 / (acked + losses);
        if (ack_rate < MIN_ACK_RATE_PERCENT)
            ack_rate = MIN_ACK_RATE_PERCENT;
    }

    rate *= 100;
    rate /= ack_rate;

    // The order here is chosen carefully to avoid overflow as much as possible
    cwnd = rate / MSEC_PER_SEC;
    cwnd *= rtt_ms;
    cwnd /= mss;
    cwnd *= brutal->cwnd_gain;
    cwnd /= 10;
    cwnd = max_t(u32, cwnd, MIN_CWND);

    brutal_tcp_snd_cwnd_set(tp, min(cwnd, tp->snd_cwnd_clamp));

    WRITE_ONCE(sk->sk_pacing_rate, min_t(u64, rate, READ_ONCE(sk->sk_max_pacing_rate)));
}

剖析

  1. 从 struct tcp_sock 检索当前时间戳,转换成秒
  2. 计算出一个最小的时间戳,该时间戳是当前时间戳减去一个固定大小的窗口(PKT_INFO_SLOTS)。这个窗口用来限定考虑的数据包信息的时间范围
  3. 遍历存储数据包信息的数组 brutal->slots,累加在时间窗口内的确认(acked)和丢失(losses)的数据包数量
  4. 如果在时间窗口内的样本数量小于定义的最小样本数量 MIN_PKT_INFO_SAMPLES,则将确认率 ack_rate 设为 100%,表示没有丢包
  5. 如果样本数量足够,则计算确认率 ack_rate,即在考虑的时间窗口内被确认的数据包与总数据包的比例,并乘以 100 转换为百分比格式
  6. 如果确认率低于定义的最小确认率 MIN_ACK_RATE_PERCENT,则使用这个最小值
  7. 然后根据确认率调整发送速率 rate。这里使用了一些数学操作来避免使用浮点数,因为内核通常不使用浮点运算
  8. 接下来,根据新的发送速率、往返时间(RTT),以及最大分段大小(MSS)计算新的拥塞窗口大小 cwnd
  9. 拥塞窗口大小还乘以了窗口增益 cwnd_gain(调整因子),然后除以10(因为增益是以十分之一为单位)
  10. 拥塞窗口大小被限制在一个最小值 MIN_CWND 和 tp->snd_cwnd_clamp(拥塞窗口的最大允许值)之间
  11. 使用 brutal_tcp_snd_cwnd_set 更新 tcp_sock 结构体中的 snd_cwnd 拥塞窗口大小
  12. 最后,更新套接字的发送速率 sk_pacing_rate,这也被限制在 sk_max_pacing_rate(最大发送速率)以下

其实我们能明显看出来,TCP 拥塞算法的核心思想,就是根据当前的网络拥塞情况(在这里依赖于判断丢包率)来计算 cwnd,这个显著指标作为核心值会判定直接影响到发送速率和数据包重传策略的决策,这种 AIMD 原则会在网络状况良好时逐渐增加发送窗口,以增大吞吐量,而在检测到网络拥塞(如丢包事件)时则大幅减小窗口大小,以减轻网络负担,理论上来说遵循这种原则的算法都可以算是一种 “君子算法”,但是 Brutal 算法不是的,在他们的文档中就说明了这一点:

阅读更多

浅谈Kubernetes Operator模式的开发

前言

这篇博客是属于对《Kubernetes Operator 进阶开发》的初步章节一个笔记总结,作为 2023 第一篇博文,也作为重新深入梳理 K8s 的基础部分,往后会逐步将之前的坑给填上

Operator的基本概念

作为最入门的篇章,我们将从写一个 Demo 开始,在这里将默认已经存在了一个可用的集群环境,当然如果在开始学习之前,你能有一些 Client-Go 基础那就再好不过了

控制器模式

控制器模式在日常生活中的应用非常广泛,也是根据原书的例子进行举例,当然这里缩减了非常多,也加入了些个人的想法

夏天我们需要调整空调制冷,现在过程如下:

  1. 空调启动后设置一个温度,例如 25 度
  2. 空调检测室温,确定室温是否高于目标值
  3. 如果高于目标阈值,例如室温 27 度,则开始制冷操作
  4. 如果低于目标阈值,保持静默到下一个探测周期

这其实就是控制器模式典型的工作流程,外部输入一个 “期望值”,期间一个 “控制器” 不断按照 “周期” 观测 “环境状态” 的 “实际值” 和 “期望值” 之间的差异,然后不断调整两者,以便维持平衡,这个过程则被称为 “调谐”

Kubernetes中的控制器与CRD

Kubernetes 中通过 “声明式API” 定义了一系列的资源对象,然后通过许多的 Controllers 来 “调谐” 这些资源对象的实际状态以向期待状态靠拢,从而实现整个集群 “尽可能” 靠拢配置中声明的期望状态,注意 CRD (Custom Resource Definition)是 Kubernetes 提供的一种机制,允许用户定义新的资源类型,通过定义 CRD,你可以扩展 Kubernetes 的 API,创建和管理自定义资源(Custom Resource,CR),Controller 则是一个自定义的控制器程序,它监视并响应 Kubernetes 集群中的资源变化。对于自定义资源,Controller 用于实现资源的创建、更新和删除逻辑

还是上面空调的例子,我们可以定义一个新的资源类型 AirConditioner,它包含空调的目标温度和当前状态等信息,CR 是基于 CRD 实例化的具体资源。每个空调的具体配置实例就是一个 CR,其包含了目标温度和当前温度等具体值,同样给空调 A (my-airconditioner-A) 和空调 B (my-airconditioner-B) 分别创建了两个温度,每个温度集合的格式都需要符合 CRD 的声明,实例中 Controller 是用于监控 CR 并执行相应操作的程序,它负责监控 AirConditioner 资源,并根据当前温度和目标温度来决定是否启动制冷

阅读更多

杂谈 - 关于我买了条跨境专线这件事情

前前言

这是一篇杂谈,意味着不会像其他技术博文一样要求思路很严谨,也或者里面会有些错误或者不可靠的地方,欢迎探讨。

前言

再又一次看到谷歌搜索的人机认证后,我叹了口气。

自从高二有了自己第一台电脑后,怀着对 Linux 极大的兴趣,通过折腾 VPS 逐渐初次接触到了 “翻墙” 这个概念,才恍然意识,原来英文互联网能获取那么多一针见血的教程/知识,原来 Youtube (那时候对哔哩哔哩还没感兴趣)的视频那么好看,加上 Google 针对百度压倒性的搜索质量等等,让我从 “简中局域网居民”(不含贬义) 逐渐变成了一位 “国际互联网公民”。 那时候 Shadowsocks 横空出世,机场的概念还没兴起,自建梯子是当时的主流,支持 AliPay 的 Vultr 凭借着最低 35 元/月的最低套餐,变成了不知道多少 “年轻人的第一台 VPS”,我犹记得刚刚开始的时候,Vultr 东京 NTT 线路节点到深圳也才 45ms,学着教程部署一套 SS 后畅游外网似乎是一套那么理所当然的操作。

然而 Vultr 被人挤满后,当时夸张到需要连开五台机器,才能找出可以正常没有被墙的 IP,只要识别到了该 IDC 东京网段,网站会屏蔽你的服务,这就是因为低门槛导致不可控的因素极具增多,不是每个人都是一名合格的邻居,爬虫/垃圾邮件/DDOS/反动内容充斥着这里时候,我们意识到是时候找条新路子了。

一年亦或者是两年后,一方面是对持续维护翻墙服务器感到了厌烦,SS/SSR 在当时属于不断升级对抗阶段,如果更新不及时被 GFW 发现协议特征(最为经典就是 TCP 重放探测了),你会发现服务器 IP 被墙并不是件很意外的事情。另一方面,机场的概念被提出,只需要一台最低价格套餐就能享受到数以十计的节点,速度拉满,这简直不要太舒服。

机场时期

机场模式将会是目前以及未来的主流。

机场模式伴随度过大学时光,Stackoverflow / Github / Google 学术等等也是借着机场的便利才得以顺通,对于大部分人来说,机场完完全全可以当成最优解,只需要刷刷 Ins 或者谷歌搜索这种用途,又何必用其他的手段呢。

然而事情的便利都是有代价的,机场低廉的价格意味着需要进行超售才能维持成本,一条号称全 BGP 线路机场套餐 300G 才 10 元,一个出口 IP 被几百人使用,注意一个合格的机场是不会直连国外落地节点的,客户线路太不可控,万一效果不理想炸起来口碑就没了。一般都是在国内放置一个 BGP 入口,可以是阿里云/腾讯云等等,三网客户汇聚后转发到对于 BGP 节点优秀的出口线路。例如 PCCW / CN2 这些。

阅读更多

在Ubuntu Server使用KVM与调优

KVM 是一种让 Linux 变成虚拟机监控程序的技术,它可以让多个虚拟机在同一台物理机上运行。为了让虚拟机正常运行,KVM 需要一些操作系统的组件,如内存管理器、进程调度程序、输入/输出堆栈、设备驱动程序、安全管理器和网络堆栈等,这些组件都包含在 KVM 中。每个虚拟机都像一个 Linux 进程一样运行,使用虚拟的硬件设备,如网卡、图形适配器、CPU、内存和磁盘等。

初始化和安装软件依赖

由于 KVM 和 Linux 内核紧密相关,所以为了保持文档的一致性,我们这里使用 5.15 版本的 Ubuntu 内核

$ uname -a
Linux sxueck-server 5.15.0-1034-realtime #37-Ubuntu SMP PREEMPT_RT Wed Mar 1 20:50:08 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
$ sudo apt update
$ sudo apt upgrade

$ sudo apt -y install bridge-utils cpu-checker libvirt-clients libvirt-daemon qemu qemu-kvm genisoimage libguestfs-tools linux-tools-common
$ echo 1024 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages # 启用hugepages

在进行任务之前,我们需要对前置条件进行检查

$ kvm-ok
INFO: /dev/kvm exists
KVM acceleration can be used

上面代表了当前的系统能良好支持 KVM 环境

阅读更多

Linux内核技术eBPF浅析

引言

最近在阅读 《Linux内核观测技术BPF》这本书,非常薄的一本书却能带领我走入 Bpf 技术的世界,但是它只能起到一个入门的作用,用于工作的时候还是有点捉襟见肘,这篇文章只能当成一个较为深入的理解,不能取代系统性的学习。

当我们使用网络应用时,性能和可靠性是至关重要的。然而,由于内核网络栈的设计和实现方式,传统的用户空间网络应用在处理大量网络流量时会面临性能瓶颈。为了解决这一问题,Linux 内核引入了一种名为 eBPF(Extended Berkeley Packet Filter)的技术,它允许开发人员在内核中运行自定义的代码,从而提高网络应用程序的性能和灵活性。而下面要重点提到的 XDP(eXpress Data Path)是 eBPF 的一个子集,提供了一种快速的数据处理方法,它通过在数据包接收阶段运行代码来提高网络应用的性能。

这里有个点需要注意一下,eBPF 和 BPF 都是一种可以在Linux内核中使用的虚拟机技术,但是它们有一些不同之处,BPF 最初是一种用于数据包过滤的技术。随着时间的推移,它的功能逐渐扩展,被用于在内核中运行的各种任务,例如系统跟踪和安全审计等。BPF 程序在内核中直接执行,它可以访问内核数据结构,但是它的功能和灵活性受到了一些限制。

而 eBPF 是对 BPF 的扩展,即 “extended BPF”。它通过引入一个新的指令集,允许在内核中运行的 BPF 程序更加灵活和强大。eBPF 可以在安全的环境下执行用户定义的代码,而不会影响内核的稳定性和安全性。eBPF 还支持在内核和用户空间之间进行通信,并提供了一些新的功能,例如支持用户定义的映射和钩子程序。

总的来说,eBPF 是 BPF 的一个更加灵活和功能强大的版本,它为内核和用户空间提供了更多的通信和交互能力,同时保持了与内核的兼容性和安全性。我们入门的话会从 BPF 开始进行解析。

微服务时代Linux内核的问题

如果论起 eBpF 最出名的案例,莫过于 Cilium 了,我参考了 Cilium 项目发起人的演讲《How to Make Linux Microservice-Aware with Cilium and eBPF》,在云原生时代,为什么 eBpf 至关重要。而他其中列出了四个为什么当前 Linux 内核不适合当前的时代(背景为内核尚未对云原生做支持的时代)。

Abstractions 抽象

Abstractions

上图是一张和网络相关的抽象,它清晰地展示了 Linux 内核中各个部分的抽象,例如如果我们想用 Netfilter 做包过滤,那就必须要经过 socket 和 TCP 协议栈。而如果从网卡往上看的话,包需要经过 Netdevice -> traffic shaping -> Ethernet 等等才能到达上部分应用。

阅读更多

祝贺站点在一周年之际获得各项技术评分优秀

年底事情太多了,加上最近 COVID-2019 的事件,我也不幸中招,一直没有时间更新博客

分数就当乐呵就行,纯当给生活增点趣味

SSL 安全评分

SSL 技术评分 A+ 级

ssl

ossl

速度

谷歌 Insights 评分满分

pc

Gtmetrix 速度测试优秀

Gtmetrix

至此也算给 2022 画上一个极其潦草的句号(明年保证将以前的坑填完!!!)

阅读更多

K3s更换底层DataStore实现简易HA

前言

K3s 自带的 SQLite 应付普通的嵌入式小服务来说绰绰有余,但是对于公司这种动辄吃掉几个核的高频调度来说,虽然勉强能支撑起日常的响应,却总有时候出现奇奇怪怪的 BUG,我已经不止一次碰到了因为默认 ServiceAccount 和 Namespace 中的资源无法做实时绑定,导致 SpringCloud 一直无法正常启动的问题了,虽然可以通过万能重启做解决,但这远算不上高可用边缘部署方案

下面是官方给 K3s 画的一副简图,我们之前已经将默认的 LB 换成了 MetaLB 并表示效果非常优秀,这次我们也来缝缝补补,按照文档来说,K3s 支持以下的外接数据库


K3s supports the following datastore options

  • Embedded SQLite
  • PostgreSQL (certified against versions 10.7, 11.5, and 14.2)
  • MySQL (certified against versions 5.7 and 8.0)
  • MariaDB (certified against version 10.6.8)
  • Etcd (certified against version 3.5.4)
  • Embedded etcd for High Availability

k3s-architecture


因为想要为以后的 Cluster Metrics 做接口准备,所以就暂时不考虑内嵌 Etcd 的方式,而是选择自己外接,这样对于整个集群的运行状况有一个直观的了解,也方便做监控 —— K3s 砍去了太多东西换取轻量化

我们都知道 K8s 的默认 Datastore 是 Etcd,而 K3s 则是使用了一种称为 Kine 的组件将 Etcd 的 K/V 操作翻译为了关系数据库的语法,Kine 将自己的接口暴露给 K3s ApiServer,也就是说,在集群组件看起来,自己还是针对 Etcd 进行读写,然而如果我们设置真正的 Etcd Backend,Kine 会略过并直接暴露真实的 Etcd-servers 给 K3s ApiServer

阅读更多

记一次服务器异常延迟问题

公司的某一台即将交付的生产环境数据库处理异步请求忽然异常缓慢,而且同事反馈说安装软件也有卡顿情况。

问题排查流程

初步尝试

首先我尝试将 PgSQL 服务进行重启,因为服务的异步处理速度延迟达到了1秒之久,况且该服务的依赖性没有那么高

$ kubectl rollout restart -n databases statefulset postgres-postgresql

结果出现了麻烦,整个 Pods 一直处于 Terminating 状态,一开始还以为是 PreStop 任务没有处理结束导致堵塞,后来发现 PgSQL 的日志已经在几分钟之前就不输出了,看起来是集群没有进行处理。

$ kubectl get event -n databases
47m         Warning   Unhealthy                pod/postgres-postgresql-0                          Liveness probe errored: rpc error: code = NotFound desc = failed to exec in container: failed to load task: no running task found: task dbd0aae87722bdababfdf720ec389aaeaab040f594b782451ea39edfbc1261bb not found: not found
47m         Warning   Unhealthy                pod/postgres-postgresql-0                          Readiness probe errored: rpc error: code = NotFound desc = failed to exec in container: failed to load task: no running task found: task dbd0aae87722bdababfdf720ec389aaeaab040f594b782451ea39edfbc1261bb not found: not found

通过查看错误事件即可发现,是清理 Container 的任务无法被正确处理,让我们手动清理一下

阅读更多

浅析Linux内核中的Apparmor机制

前言

最近有在帮公司制作一个 OpenVpn Snap 安装包,然而频繁遇到了 Apparmor 错误,其实之前安装 Shadowsock 包的时候也见过,但是当时没怎么太认真探究,现在回看起来发现国内这方面的相关资料还是比较少的,本篇博文从介绍到实践由浅到深,帮助自己也帮助读者理解。

错误样例

$ sudo snap install easy-openvpn-server_0.2_amd64.snap --dangerous
error: cannot perform the following tasks:
- Setup snap "easy-openvpn-server" (unset) security profiles (cannot setup apparmor for snap "easy-openvpn-server": cannot unload apparmor profile: exit status 2
apparmor_parser output:
File snap-update-ns.easy-openvpn-server not found, skipping...
File snap.easy-openvpn-server.easy-openvpn-server not found, skipping...
File snap.easy-openvpn-server.hook.configure not found, skipping...
File snap.easy-openvpn-server.hook.connect-plug-firewall-control not found, skipping...
File snap.easy-openvpn-server.hook.connect-plug-network-control not found, skipping...
File snap.easy-openvpn-server.openvpn not found, skipping...
File snap.easy-openvpn-server.status not found, skipping...
File snap.easy-openvpn-server.tcp-server not found, skipping...
File snap.easy-openvpn-server.udp-server not found, skipping...
)
- Setup snap "easy-openvpn-server" (unset) security profiles (cannot unload apparmor profile: exit status 2
apparmor_parser output:
File snap-update-ns.easy-openvpn-server not found, skipping...
File snap.easy-openvpn-server.easy-openvpn-server not found, skipping...
File snap.easy-openvpn-server.hook.configure not found, skipping...
File snap.easy-openvpn-server.hook.connect-plug-firewall-control not found, skipping...
File snap.easy-openvpn-server.hook.connect-plug-network-control not found, skipping...
File snap.easy-openvpn-server.openvpn not found, skipping...
File snap.easy-openvpn-server.status not found, skipping...
File snap.easy-openvpn-server.tcp-server not found, skipping...
File snap.easy-openvpn-server.udp-server not found, skipping...
)
- Run configure hook of "easy-openvpn-server" snap if present (run hook "configure": cannot open directory /tmp/snap.0_easy-openvpn-server_XXXXXX: Permission denied)

什么是AppArmor?

1. 安全模型描述

在谈论一个具体的实现之前,避免不了需要了解一下较为完整的背景,AppArmor 作为 Liunx 安全模型中的点亮的某个技能树叉,从操作系统的权限管理与访问机制来入手是一个比较好的选择。其基础的权限管理与访问控制机制和加强的 ACL 机制、SELinux,都对这些安全模型与安全机制有所实现。

阅读更多

驾考学习与总结

距离考试倒计时只剩下约 3 天,可是几乎没怎么学习,就是把 500 题刷了一遍,视频过了一遍而已,干脆和学计算机一样来个总结

扣分须知

目前国内考试基本用的是四月一日后新出的新规题目,经常会遇到视频和实际考题不一致的情况,这里重新巩固一下对于扣分的理解

扣一分的情况

最轻微或者不会造成事故的扣分,这一档次也是我们生活中时时刻刻会触犯到的

  • 不按规定使用灯光
  • 不系安全带
  • 普通道路不按规定倒车/掉头,或者超速 10%-20%
  • 违反禁令标志禁止标线(例如压线等情况)
  • 普通客车未按规定年检就上路
  • 货车超载不超过 30%,或者擅自改变已登记结构

做题的时候,想一下自己在路上是不是经常触碰到某些行为,或者事件发生在普通道路或者载货汽车危害不严重的时候,基本就是一分没跑了

扣三分的情况

这一类扣分行为是种类最多也是易错点最多的,如果我们想要强行总结,可以概括为 此类行为可能造成危害,且危害后果是可以预见的

  • 开车打电话
  • 不避让校车:小朋友多且出事情后果相当严重,但出事概率不算大
  • 不避让行人
  • 不按规定超车
  • 快速与高速路段不按规定车道行驶
  • 出故障后停车不按规定使用灯光:可能会造成后车误判导致追尾
  • 高速行驶低于最低时速:说的就是你,五菱牌移动路障
  • 货车违反 420 法制(开车大于 4 小时后没有休息 20 分钟):不过我感觉这个挺难界定的
  • 不按规定安装号牌:咋地,照不到牌就没法扣你分了吗,当然前提是号牌要完好
  • 需要AB照才能开的车没有按规定年检就上路:C照是1分

你看,3分基本都是与别人交互导致的事故,1分多是半惩戒性质,例如不打转向灯这种

同时注意,扣分体系中,高速/快速普通道路 有的时候是两套规则,载客汽车客运车/危险物品运输车 也是两套,涉及客运车这种即使是最轻微的未按规定年检上路,就已经从3分起扣了

3分这个区间还有一个关键区域值 – 20%-50%,将该区域套到普通道路与载客汽车上,即为3分

  • 普通道路逆行 / 超速20%-50%
  • 载客汽车超过核定人数 20%-50%
  • 载货汽车超重 30%-50%: 30% 以下1分
  • 载货汽车用来载客:对,躲货拉拉后备箱要扣三分
  • 校车上路前未做车况检查 / 带隐患上路

扣六分的情况

六分一般会出现在出现在,一旦出事则会导致重大危险,且本身事故概率极大的场景中,亦或者是 AB 照相关车辆违规

  • 不遵守交通信号灯,例如闯红灯
  • 占用应急车道:会导致特种车无法及时作业
  • 驾驶证被暂扣期间驾驶机动车
  • 逃逸致人轻伤 / 财产损失:逃逸最轻的后果
  • 载运危险物品车辆未按照指定的时间、线路、速度行驶或者未挂载警示标志

下面开始涉及到高速或者 50% 以上的区间

阅读更多

Go Grpc使用说明书-Protoc篇

前言

熟悉这个博客的朋友都知道,我一般都不会写语言类的说明书,但这次由于网络资料参差不齐,中间走了许多弯路,特此进行一次记录

故事背景

公司的工业机器人需要做车端行为树的汇总,系统组配合那边给出来一个 Grpc 接口(C++写),我们这边调用函数,车端进行充电等行为时候会触发行为树操作,而我们负责监听即可,本来是一个非常简单的需求。

项目准备

在这里推荐写代码还是使用 Linux 或者 Mac 更好,Windows 的配置实在是太折磨人了。

我们先来看看最终的,也就是实际跑起来的项目结构:

含有注释的都是重点文件

➜  tree
.
├── compatibility.go
├── config.go
├── debug
│   └── main.go # Server
├── go.mod
├── go.sum
├── grpc.go # 主要的接受监听部分代码,也可以看成 Client
├── grpc_test.go # 测试代码
├── main.go
├── mongo.go
├── xx.api.rfi # grpc 包名,要和调用方保持一致
│   ├── behavior_tree_engine_grpc.pb.go # 主要实现的方法
│   └── behavior_tree_engine.pb.go
├── mqtt.go
├── proto
│   └── behavior_tree_engine.proto # ProtoBuf 定义文件
└── README.md

3 directories, 14 files

在开始编写代码之前,我们先要安装对应的配套工具

阅读更多

利用 Google MT3 模型进行乐谱识别 - 本地篇

前言

最近经常看 Youtube 有很多的钢琴家在分享他们的曲子,有的时候很想将其稍微做一点修改,但是这样的话就要扒谱到 FlStudio 里面,即使有 MIDI 键盘辅助,手工扒谱也是一项非常吃力的活,于是就想着能不能借助 AI,这不,Google MT3 模型就出现在了我的眼前。

需要注意的是,MT3 项目并不是 Google 官方推出的,而是 Google 使用了 T5X 训练框架而已,T5X 框架是一个研究友好的框架,可以用于高性能、可配置、自助式训练、评估和序列模型(从语言开始)的推理,而 MT3 是一个多乐器自动音乐转录模型。

环境

我的配置单

  • 运行环境:WSL2 Ubuntu 22.04(我甚至不愿意用虚拟机)
  • 显卡:NVIDIA RTX 3080
  • CPU:Intel i9-11900K
  • 内存:64G

安装模型相应的依赖

我们需要安装如 Python、TensorFlow、NumPy、Pandas 等依赖项目

$ sudo apt-get update
$ sudo apt install \
    python3 python3-pip python3-dev python3-venv \
    gcc g++ make build-essential \
    libicu-dev libbz2-dev liblzma-dev \
    libssl-dev libxml2-dev libxslt-dev ffmpeg libsndfile1-dev
$ python3 -m pip install --upgrade pip
$ pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

接下来如果需要使用 GPU 进行加速处理的话,我们需要安装 CUBA 软件包,下面的安装方式只是一个参考,更具体的信息可以查阅 Ubuntu 官方网站

阅读更多

TCP拥塞控制算法BBR的原理和改进实践

建议就着咖啡观看,避免干着

实验环境

  • 操作系统:Debian 10
  • Linux Kernel:5.10.x

前言

我在编写之前的文章《服务器性能优化之网络性能优化》中,其实有想把 BBR 作为一种优化手段写进去的,然而受限于篇幅不能过多讲解,因此特地单独细致地研究一遍。

通常我们在研究 BBR 算法的时候,会很难不将其与 Cublic 算法做比较,作为 Linux 内核默认的 TCP 拥塞控制算法,我们在这里也将会一并列入对比和实验中。

基本的概念

盖房先建地基,对应的学习一门知识也要先掌握与巩固前置基础,这里先将几个较为关键和频繁出现的知识要点来回顾一下,但在这里我将默认你已经掌握了基本的网络技术(例如滑动窗口,重传定时器等概念),否则你更需要的可能是一本网络入门书

拥塞控制四板斧

由于 TCP 协议向应用层提供不定长的字节流发送方法,使得 TCP 协议先天性地有意愿去占满网络中的整个带宽,这时候当网络中许多连接同时试图去占满整个带宽的时候,就有可能发生恶性拥塞事件,因此 TCP 拥塞控制算法的作用,它能有效防止过多的数据注入到网络中,导致出现网络负载过大的情况。当 TCP 拥塞控制算法无法满足当前互联网应用对网络传输高实时性、高带宽利用率、高吞吐量的需求,在这种背景下 BBR 应运而生。

注意它和流量控制的区别,流量控制更多是作用于接收者,它控制发送者的发送速度从而使接收者来得及接收,防止数据分组丢失

在我们逐步分析对应控制手段之前,需要先行假设:

  1. 数据是单方向传递,另一个窗口只发送确认
  2. 接收方的缓存足够大,因此发送方的窗口大小由网络的拥塞程度来决定

慢启动

概念

我们都有过从互联网下载东西的经历,通常来说,文件的下载速率并不是一开始就达到到你的宽带上限,而是逐步从例如 1MB/s → 4MB/s 递增上去,这其实就是我们能看见的一种很典型的慢启动策略。

在这场下载中,发送方(即内容提供服务器)会维持一个叫做拥塞窗口 cwnd 的状态变量。它用来表示发送方在得到接收方确认前,最大允许传输的未经确认的窗口。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。

这时候我们又要引入两个新的词,即

  • 通告窗口 rwnd
  • 发送窗口 swnd = min(cwnd, rwnd)

虽然是新的词,但概念大体是新瓶装老酒,通告窗口就是我们 TCP 报文头的 Window 字段,也就是对方的接收窗口,例如我这里截了一张随手抓的报文内容,这里的 rwnd 就为 1021

而发送窗口的概念就更简单了,它将 rwnd 与 cwnd 做比较,取两者最小值,如果我们将整条连接堪称一个水桶,那么它代表水桶中最短的那块木板,而 cwnd 与 rwnd 相比不同的是:它只是发送方的一个内部记数,无需通知给接收方,其初始值往往会比较小,然后随着报文被接收方确认,窗口成倍扩大,有点类似拳击比赛,开始时候不了解对方情况只能先行试探,后来心里有底了逐渐加大进攻力度。

阅读更多

使用Lua编写插件加强Nginx - 初探

虽然说 Nginx 的标准模块非常强大,然而如果遇到了一些不能灵活适应系统要求的功能时候,我们往往会考虑使用 Lua 拓展和定制 Nginx 服务,目前对于这种需要拓展性的项目一般采用 OpenResty ,按照官方的介绍,OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态Web 应用、Web 服务和动态网关

但是,有的时候服务已经部署完成,包括配置都已经写好了,直接对线上的 Nginx 进行替换显然是一种非常冒险与费神的行为,又或者是只需要某一项功能的拓展,并不需要整体做大改动,这个时候用 Lua 模块就是一种很好的选择。

模拟线上环境

这里我启用了虚拟机作为线上的机器,因此需要重新安装 Nginx,Arguments 这边是需要单独记下来的,和后面编译做到尽量一个参数,这样对配置文件不会造成任何影响

$ sudo add-apt-repository ppa:nginx/stable
$ sudo apt-get update
$ sudo apt install nginx

$ nginx -V
nginx version: nginx/1.18.0 (Ubuntu)
built with OpenSSL 1.1.1f  31 Mar 2020
TLS SNI support enabled
configure arguments: --with-cc-opt='-g -O2 -fdebug-prefix-map=/build/nginx-d4S6DH/nginx-1.18.0=. -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -fPIC' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --modules-path=/usr/lib/nginx/modules --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-compat --with-debug --with-pcre-jit --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module --with-threads --with-http_addition_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_sub_module

编译包含了 Lua 模块的 Nginx

$ mkdir nginx && cd nginx
$ wget https://nginx.org/download/nginx-1.18.0.tar.gz
$ tar xvf nginx-1.18.0.tar.gz

不推荐去 LuaJIT 官网下载 2.0.5 版本,不然在 arm64 下将无法进行编译(M1 Mac)

阅读更多

从GoProxy中学习反向代理

阅读目的

弄清楚项目是如何完成代理的功能,因此重点弄懂 Proxy 部分的代码

项目地址

https://github.com/goproxyio/goproxy

从项目启动开始,我们的重点就是下面的这个参数

flag.StringVar(&proxyHost, "proxy", "", "next hop proxy for Go Modules, recommend use https://gopropxy.io")

我们将这个参数设置为国内可以访问的 https://goproxy.cn ,并且尝试一下是否能跑通整个服务,这里使用的是 Powershell,换成 Linux Shell 的时候其实也是一样的流程

# 单独开启一个终端启动服务
$ $CacheDIR = "G:\\GomodDebug"
$ mkdir -p $CacheDIR\pkg\mod\cache\download\github.com\goproxyio\goproxy\@v\
$ go run . -proxy https://goproxy.cn -listen 127.0.0.1:8085 -cacheDir $CacheDIR
goproxy.io: ProxyHost https://goproxy.cn
goproxy.io: ------ --- /github.com/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/goproxy/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/@v/list [proxy]
goproxy.io: 0.344s 404 /github.com/@v/list
goproxy.io: 0.548s 200 /github.com/goproxyio/goproxy/@v/list
goproxy.io: 0.619s 404 /github.com/goproxyio/@v/list
goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/cfg/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/module/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/modfetch/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/modfetch/codehost/@v/list [proxy]
goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/modload/@v/list [proxy]
goproxy.io: 0.428s 404 /github.com/goproxyio/goproxy/internal/@v/list
goproxy.io: 0.473s 404 /github.com/goproxyio/goproxy/internal/module/@v/list
goproxy.io: 0.532s 404 /github.com/goproxyio/goproxy/internal/modfetch/@v/list
goproxy.io: 0.555s 404 /github.com/goproxyio/goproxy/internal/modload/@v/list
goproxy.io: 0.592s 404 /github.com/goproxyio/goproxy/internal/cfg/@v/list
goproxy.io: 0.697s 404 /github.com/goproxyio/goproxy/internal/modfetch/codehost/@v/list

# 这是IDE中正常的测试终端
$ go env -w GO111MODULE=on
$ go env -w GOPROXY=http://127.0.0.1:8085

# 我们上面并没有设置direct,意味着拉取源只会通过代理
$ go get -u github.com/goproxyio/goproxy
github.com/goproxyio/goproxy imports
github.com/goproxyio/goproxy/pkg/proxy imports
...

深入研究Proxy的流程

Director

handle = &logger{proxy.NewRouter(proxy.NewServer(new(ops)), &proxy.RouterOptions{
			Pattern:      excludeHost,
			Proxy:        proxyHost,
			DownloadRoot: downloadRoot,
			CacheExpire:  cacheExpire,
})}

func NewRouter(srv *Server, opts *RouterOptions) *Router {
	rt := &Router{
		opts: opts,
		srv:  srv,
	}
	if opts != nil {
		if opts.Proxy == "" {
			log.Printf("not set proxy, all direct.")
			return rt
		}
		remote, err := url.Parse(opts.Proxy)
		if err != nil {
			log.Printf("parse proxy fail, all direct.")
			return rt
		}
		proxy := httputil.NewSingleHostReverseProxy(remote)
		director := proxy.Director
		proxy.Director = func(r *http.Request) {
			director(r)
			r.Host = remote.Host
		}

		rt.proxy = proxy

		rt.proxy.Transport = &http.Transport{
			Proxy:           http.ProxyFromEnvironment,
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		}
		rt.proxy.ModifyResponse = rt.customModResponse
		...
	}
	return rt
}

如果没有设置代理或者代理没有通过 url.Parse 的解析 ,服务将会提示将会使用直连模式

阅读更多

如何编写一款网络代理服务

本篇文章仅供技术交流,请遵循相关国家法律法规,作者不提供任何技术支持
没啥思路,文章暂时搁置 - 2022-03-02

前言

Socks5 协议

在编写相关代码之前,我们需要先将 Socks5 协议给了解一下

使用 WireShark 抓取 Socks5 报文

$ curl --socks5 157.90.140.29:1080 ip.sb

同时我也将 WireShark 抓取到的报文片段截取下来了,可以点击 下载 后,使用相同的软件进行本地分析

分析Go-Shadowsocks2的源码

tcp.go

// Create a SOCKS server listening on addr and proxy to server.
func socksLocal(addr, server string, shadow func(net.Conn) net.Conn) {
	logf("SOCKS proxy %s <-> %s", addr, server)
	tcpLocal(addr, server, shadow, func(c net.Conn) (socks.Addr, error) { return socks.Handshake(c) })
}

// Create a TCP tunnel from addr to target via server.
func tcpTun(addr, server, target string, shadow func(net.Conn) net.Conn) {
	tgt := socks.ParseAddr(target)
	if tgt == nil {
		logf("invalid target address %q", target)
		return
	}
	logf("TCP tunnel %s <-> %s <-> %s", addr, server, target)
	tcpLocal(addr, server, shadow, func(net.Conn) (socks.Addr, error) { return tgt, nil })
}
  • addr:客户端监听地址
  • server:客户端连接的地址,也就是远程服务器的地址

而关于 shadow 这个传入的匿名函数,我们可以通过其他地方是如何调用它的来进行作用判断

阅读更多

经验杂谈

这里汇集了平常学习和工作中遇到的一些小小的疑难杂症

Linux Service

Nginx

xxx.so is not binary compatible in /etc/nginx/nginx.conf
这个通常发生在编译 Nginx 模块后,我们通过 load_module 加载该模块发出的报错,针对这种的解决方案也很简单,加上 --with-compat 即可,例如
./configure --with-http_image_filter_module=dynamic --with-compat

Kubernetes

数据库中间件问题

MongoDB

ReplicaSetNoPrimary 问题

错误日志:

server selection error: server selection timeout, current topology: { Type: ReplicaSetNoPrimary, Servers: [{ Addr: xxx:27017, Type: Unknown, Last error: connection() error occured during connection handshake: connection(xxx:27017[-127]) socket was unexpectedly closed: EOF }, { Addr: xxx:27017, Type: Unknown, Last error: connection() error occured during connection handshake: connection(xxx:27017[-128]) socket was unexpectedly closed: EOF }, ] }

从表面上看,代码 SDK 连接到了 Mongo 后,Mongo 返回来一个 ReplicaSet 集合,其中里面没有我可以访问的地址(返回了集群 POD 的 Name),其主要的原因是使用的 SDK 较老,那时候还没有集群化管理这种东西,不具备服务发现的功能 解决方案: In contrast to the mongo-go-driver, by default it would perform server discovery and attempt to connect as a replica set. If you would like to connect as a single node, then you need to specify connect=direct in the connection URI. 例如,在 PyMongo 的连接中加入 directConnection = True,或者类似方案

阅读更多

服务器性能优化之网络性能优化

这篇文章,将会从网络原理开始,一直到 Linux Kernel 的相关配置对于网络性能的影响,算是一个偏整体的知识学习

文章非常长,这里将大纲列出来

  • 网卡收发包队列技术优化
  • 单网卡队列 RPS / RFS 原理
  • TCP Socket 建立连接相关内核参数优化
  • TCP 连接拥塞控制相关内核参数优化
  • TCP 拥塞控制算法 - BBRv2 原理
  • TCP 显式拥塞通知 ECN 算法原理
  • TCP 快速启动解决方案

概念梳理

学习过程中可能会碰到非常多的概念和术语,在此先对这些进行一个梳理,也算是一个复习

中断和软中断

首先我们需要对中断和软中断有个起码的认知,中断一般是指硬件中断,多由系统自身或与之链接的外设(如键盘、鼠标、网卡等)产生,中断首先是处理器提供的一种响应外设请求的机制,是处理器硬件支持的特性,一个外设通过产生一种电信号通知中断控制器,中断控制器再向处理器发送相应的信号。处理器检测到了这个信号后就会打断自己当前正在做的工作,转而去处理这次中断。而内核收到信号后会调用一个称为中断处理程序(interrupt handler)或中断服务例程(interrupt service routine)的特定程序。中断处理程序或中断服务例程可以在中断向量表中找到,这个中断向量表位于内存中的固定地址中。CPU处理中断后,就会恢复执行之前被中断的程序。

整个流程可以归纳为:硬件设备 (产生中断)-> 中断控制器(通知) -> CPU -> 中断内核 -> do_IRQ() -> handler_IRQ_event -> 中断控制器(收到恢复通知)-> IRQ_exit

现在我们对于基本的中断有了个理解,从实际场景来说中断请求的处理程序应该要短且快,这样才能减少对正常进程运行调度地影响,而且中断处理程序可能会暂时关闭中断,这时如果中断处理程序执行时间过长,可能在还未执行完中断处理程序前,会丢失当前其他设备的中断请求。

所以 Linux 系统为了解决中断处理程序执行过长和中断丢失的问题,将中断过程分成了两个阶段,分别是 上半部分下半部分

  • 上半部用来快速处理中断,一般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者时间敏感的事情。
  • 下半部用来延迟处理上半部未完成的工作,一般以 内核线程 的方式运行。

例如网卡收包这里,网卡收到网络包后,会通过硬件中断通知内核有新的数据到了,于是内核就会调用对应的中断处理程序来响应该事件,这个事件的处理也是会分成上半部和下半部。

上部分要做到快速处理,所以只要把网卡的数据读到内存中,然后更新一下硬件寄存器的状态,比如把状态更新为表示数据已经读到内存中的状态值。接着,内核会触发一个软中断,把一些处理比较耗时且复杂的事情,交给「软中断处理程序」去做,也就是中断的下半部,其主要是需要从内存中找到网络数据,再按照网络协议栈,对网络数据进行逐层解析和处理,最后把数据送给应用程序。

所以,中断处理程序的上部分和下半部可以理解为:

  • 上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行
  • 下半部是由内核触发,也就说软中断,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行

网卡收发过程

网卡的收发其实重点应该在中断的下半部,我们一拍脑袋,想到接收数据包的一个经典过程:

  1. 数据到达网卡
  2. 网卡产生一个中断给内核
  3. 内核使用I/O指令,从网卡I/O区域中去读取数据

但是这种方法有一个很大的问题,就是当大流量数据到来时候,网卡会产生大量的中断,内核在处理中断上下文中,会浪费大量资源来处理中断本身。所以,NAPI 技术被提出来,所谓的 NAPI 技术,其实就是先将内核屏蔽,然后每隔一段时间去轮询网卡是否有数据。不过相应的,如果数据量少,轮询本身也会占用大量不必要的 CPU 资源,所以需要进行抉择判断。

阅读更多

编写一个带 SASL 认证的Kafka镜像

要求说明:

Kafka Brokers 不管是互相通信,对外认证,还是和 Zookeeper 的交互进行,都需要使用 SASL/PLAIN 进行认证

思路:

Kafka 的 bin 目录下面,包含了 Zookeeper 和 Kafka Server 的启动脚本,我们只需要在脚本运行之前,通过环境变量 KAFKA_OPTS 指定对应的 JAAS 认证文件,最后启动 properties 文件即可,理清了整个环节之后还是非常简单的

操作开始

目录结构

/images/kafka-sasl/Untitled.png

首先需要编写 Dockerfile 作为环境变量声明的地方,不然配置文件就没法使用模板进行编排了

FROM openjdk:8-jre-slim

RUN set -eux; \
    apt -y update && apt -y install procps; \
    apt clean

WORKDIR /

ENV STORE_DATA="/kafka_2.12-1.1.1/store/data" \
    STORE_LOGS="/kafka_2.12-1.1.1/store/logs" \
    ZOO_USER="ZookeeperUsername" \
    ZOO_PASS="ZookeeperPassword" \
    ZOO_QUORUM_USER="QuorumUserName" \
    ZOO_QUORUM_PASS="QuorumUserPassword" \
    KAFKA_USER="KafkaUsername" \
    KAFKA_PASS="KafkaPassword" \
    KAFKA_CLIENT_BROKER_USER="KafkaClientBrokerUsername" \
    KAFKA_CLIENT_BROKER_PASS="KafkaClientBrokerPassword" \
    ZOOKEEPER_CONNECT="localhost:2181" \
    MODE="" \
    # create multiple users with different permissions
    # operation attribute: Read Write *
    # KAFKA_ACCOUNT_GROUP "user1|password|* readonlyuser|password|Read"
    KAFKA_ACCOUNT_GROUP="" \
    # each pod contains a zk and a kafka
    # so their number of nodes and information should be the same
    CLUSTER_NODE=""
# CLUSTERNODE "node0|0|127.0.0.1 node1|1|127.0.0.1"

ENV PATH=$PATH:/kafka_2.12-1.1.1/bin \
    ZOO_DATA_DIR="$STORE_DATA/zookeeper/data" \
    ZOO_MYID_FILE="$STORE_DATA/zookeeper/data/myid" \
    ZOO_DATA_LOG_DIR="$STORE_LOGS/zookeeper/logs/dataLogs" \
    ZOO_LOG_DIR="$STORE_LOGS/zookeeper/logs" \
    SERVER_JVMFLAGS="-Djava.security.auth.login.config=/kafka_2.12-1.1.1/config/zoo_server_jaas.conf" \
    KAFKA_OPTS="-Djava.security.auth.login.config=/kafka_2.12-1.1.1/config/kafka_server_jaas.conf" \
    ZOO_CFG="/kafka_2.12-1.1.1/config/zookeeper.properties" \
    Z00_AUTH="/kafka_2.12-1.1.1/config/zoo_server_jaas.conf" \
    KAFKA_CFG="/kafka_2.12-1.1.1/config/server.properties" \
    KAFKA_AUTH="/kafka_2.12-1.1.1/config/kafka_server_jaas.conf" \
    KAFKA_DATA_LOG_DIR="$STORE_LOGS/kafka/logs/dataLogs"
ENV ENV_CONFIGRUE="ZOO_DATA_DIR ZOO_DATA_LOG_DIR ZOO_USER ZOO_PASS ZOO_QUORUM_USER ZOO_QUORUM_PASS KAFKA_USER KAFKA_PASS KAFKA_CLIENT_BROKER_USER KAFKA_CLIENT_BROKER_PASS KAFKA_DATA_LOG_DIR ZOOKEEPER_CONNECT" \
    ENV_CONFIGFILE="ZOO_CFG Z00_AUTH KAFKA_CFG KAFKA_AUTH" \
    POD_NAME=""

ADD kafka_2.12-1.1.1.tgz .
COPY zookeeper.properties server.properties kafka_server_jaas.conf zoo_server_jaas.conf /kafka_2.12-1.1.1/config/

RUN set -eux;\
    groupadd -r kafka --gid=1000; \
    useradd -r -g kafka --uid=1000 kafka; \
    mkdir -p "$ZOO_DATA_DIR" "$ZOO_DATA_LOG_DIR" "$ZOO_LOG_DIR" ; \
    chown -R kafka:kafka /kafka_2.12-1.1.1/

COPY --chown=root:root  docker-entrypoint.sh pre-stop.sh /
USER kafka:kafka
CMD ["/bin/bash", "/docker-entrypoint.sh"]

这里声明了我们需要被用到的一些通用环境变量,还有镜像的一个构建方式

阅读更多

使用BlackBox Exporter实现服务可用性检测

生产环境案例

场景分析

我们需要对三个集群进行监控,分别是

  • 开发-测试集群 : blackbox-dev
  • 预发布集群 : blackbox-pre
  • 生产集群 : blackbox-pro

/images/blackexporter/Untitled.png

Blackbox Exporter 配置

首先需要使用 BlackBox Exporter 实现一个探测 Module ,这个 Module 可以包含 HTTP、HTTPS、SSH 等方案,我们对目标进行监控的时候,可以直接使用 Prometheus 传入 Targets 以调用 Module

运行 Blackbox Exporter 时,需要用户提供探针的配置信息,这些配置信息可能是一些自定义的 HTTP 头信息,也可能是探测时需要的一些 TSL 配置,也可能是探针本身的验证行为,在 Blackbox Exporter 每一个探针配置称为一个 module,并且以 YAML 配置文件的形式提供给 Blackbox Exporter

如果我们想调用集群中的某个服务,必须先经过一个 API 网关,这个 API 网关兼备了访问控制的功能,每个用户需要在 Request 请求头中添加一个 Bearer Token —— 这取决于网关的具体配置,我们的探针也不例外,下面是 BlackBox Exporter 的配置示例

blackbox.yml

modules:
  http_2xx:
    prober: http
    timeout: 5s
    http:
      preferred_ip_protocol: ip4
      headers:
        Host: "probe.xxx.com"
        Cache-Control: no-cache
      bearer_token: '<token>'
  tcp_connect:
    prober: tcp
    timeout: 5s
    tcp:
      preferred_ip_protocol: ip4
  icmp:
    prober: icmp
    timeout: 5s
    icmp:
      preferred_ip_protocol: ip4
  http_2xx_home:
    prober: http
    timeout: 5s
    http:
      method: GET
      fail_if_body_not_matches_regexp:
      - "OK"
      fail_if_not_ssl: false
      preferred_ip_protocol: ip4

将服务启动后,我们访问 IP:9115 就能看见 WebUI 了

阅读更多

从 Echo Demo 模仿一个 Go HTTP 框架

因为 Echo 框架本身是没有 Main 入口的,我们只能通过官方给的一个小的基础 Demo,作为源码阅读的起点

Demo - Hello World

package main

import (
	"net/http"
	
	"github.com/labstack/echo/v4"
)

func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":1323"))
}

解析

入口 - New

func New() (e *Echo) {
	e = &Echo{
		Server:    new(http.Server),
		TLSServer: new(http.Server),
		AutoTLSManager: autocert.Manager{
			Prompt: autocert.AcceptTOS,
		},
		Logger:          log.New("echo"),
		colorer:         color.New(),
		maxParam:        new(int),
		ListenerNetwork: "tcp",
	}
	e.Server.Handler = e
	e.TLSServer.Handler = e
	e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
	e.Binder = &DefaultBinder{}
	e.Logger.SetLevel(log.ERROR)
	e.StdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0)
	e.pool.New = func() interface{} {
		return e.NewContext(nil, nil)
	}
	e.router = NewRouter(e)
	e.routers = map[string]*Router{}
	return
}

该函数是整个框架的入口,也是用户调用的第一步,它返回了一个 Echo 接口,并同时初始化了几个参数

阅读更多

Go Mutex 源码随笔

编写时间 2020.10 月,当时技术有限深度还是不足,文章仅提供思路

共享内存和信号量

共享内存是进程间通信最简单的方式之一,允许两个或更多进程访问同一块内存,我们可以通过给不同进程返回指向同一个物理内存区域的指针来做到这一点, 当一个进程改变了这块地址中的内容的时候,其他进程都会察觉到这个更改。

但是使用共享内存的时候,若一个进程正在向共享内存写入数据,则在它完成这一步操作之前,别的进程都不应该去读写这些数据,否则会造成数据覆盖或者难以预料的错误。

为了防止多进程竞争共享资源而造成的数据错乱,需要一个保护机制使得共享的资源在任意时刻只能被一个进程访问,正好信号量就实现了这一保护机制。

信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据

信号量表示资源的数量,控制信号量的方式有两种原子操作:

  • P 操作:这个操作会把信号量 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需堵塞等待,如果信号量 ≥ 0,代表还有资源可用,进程可继续正常运行
  • V 操作,这个操作会把信号量加上 1,相加后如果信号量 ≤ 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程

P 操作是在进入共享资源之前,V 操作是在离开共享资源之后,这两个操作必须同时出现

Mutex初级版本源码解读

我从 Github 上能拉到最早的带有 Mutex 的相关代码为 Go 1.4,加上注释也才 109 行,非常小巧

使用示例

mutex := &sync.Mutex{}
mutex.Lock()
// ..do something
mutex.Unlock()

和 Slice 一样,Go 中的 Mutex 本质上还是一个结构体,而且零值 Mutex 为未上锁状态,Mutex一旦被使用禁止被拷贝

其中 Mutex 中的 state 字段代表当前锁的状态,而 sema 代表控制锁的信号量

type Mutex struct {
	state int32
	sema  uint32
}

const (
	mutexLocked = 1 << iota // mutex is locked
	mutexWoken
	mutexWaiterShift = iota
)

state 是一个 32 位的整型字段

阅读更多

阿里云 ACK 集群搭建内外网分流 KAFKA

正常来说,直接官方 Helm 使用 Service + ClusterIP 的在集群的搭建模式会导致分布式多节点的时候无法对集群外部提供服务,我们可以将错误复现一遍

正常模式的错误复现

首先存在一个官方给出的正常 Helm 模版,并且已经都配置好了 Zookeeper 依赖

total 168
drwxr-xr-x 5 root root  4096 Feb 18 11:05 .
drwxr-xr-x 3 root root  4096 Feb 18 10:51 ..
drwxr-xr-x 2 root root  4096 Feb 18 10:51 charts
-rw-r--r-- 1 root root   548 Feb 18 10:51 Chart.yaml
drwxr-xr-x 3 root root  4096 Feb 18 10:51 files
-rw-r--r-- 1 root root   333 Feb 18 10:51 .helmignore
-rw-r--r-- 1 root root 67927 Feb 18 10:51 README.md
-rw-r--r-- 1 root root   227 Feb 18 10:51 requirements.lock
-rw-r--r-- 1 root root   137 Feb 18 10:51 requirements.yaml
drwxr-xr-x 2 root root  4096 Feb 18 10:51 templates
-rw-r--r-- 1 root root 30782 Feb 18 10:51 values-test.yaml
-rw-r--r-- 1 root root 30932 Feb 18 11:02 values.yaml

具体看关键的 listeners 配置

阅读更多

手动更换 K8s APIServer 的证书

当我们遇到证书过期,或者遇到下面的情况的时候,可能需要手动更换一下证书了

Unable to connect to the server: x509: certificate is valid for 10.43.0.1, 127.0.0.1, 192.168.0.2, not xxx

查看并备份当前证书

这里以 K3s 为例,原理和 K8s 完全相同

$ cd /var/lib/rancher/k3s/server/tls
$ openssl x509 -noout -text -in serving-kube-apiserver.crt
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 5436315453726641788 (0x4b71ac1a3257ce7c)
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: CN = k3s-server-ca@1640660897
        Validity
            Not Before: Dec 28 03:08:17 2021 GMT
            Not After : Dec 28 03:08:17 2022 GMT
        Subject: CN = kube-apiserver
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:53:50:c3:aa:83:af:d5:0c:13:a2:b4:55:09:28:
                    de:c6:65:b3:62:e6:78:06:90:22:69:b3:42:b5:e2:
                    5f:ed:f2:7d:4c:bc:a0:bc:ea:b5:ee:82:5e:36:16:
                    65:ad:7e:03:e0:73:ef:f3:26:35:8f:2e:36:d8:cf:
                    6a:0e:70:f4:b8
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Authority Key Identifier:
                keyid:CC:B5:B8:3B:36:D9:2D:F0:E1:E2:F0:01:C5:85:A2:69:ED:1C:19:BD

            X509v3 Subject Alternative Name:
                DNS:kubernetes, DNS:kubernetes.default,
								DNS:kubernetes.default.svc, 
								DNS:kubernetes.default.svc.cluster.local, DNS:localhost, DNS:k8smaster, 
								IP Address:127.0.0.1, IP Address:192.168.0.2, IP Address:10.43.0.1
    Signature Algorithm: ecdsa-with-SHA256
         30:45:02:20:78:49:86:38:cc:65:c2:0a:38:83:1f:98:84:1f:
         50:85:4c:71:db:35:56:7c:af:44:3a:15:58:98:58:f9:e6:89:
         02:21:00:fb:69:0b:66:a1:b8:c3:92:21:a6:23:cf:ed:19:03:
         26:fc:f1:bd:b7:d9:3a:50:d8:4b:01:90:cf:c9:8a:8b:19

可以看到,我们这个证书的有效期为一年,同时 X509v3 Subject Alternative Name 这边,也有着对应的签名

阅读更多

OSI 模型简介

当时写给朋友的网络基础

前言

对于我们每天使用的互联网来说,你有没有想过,它的底层是如何被实现的?

你的打开手机浏览器,想要搜索点什么,有没有想过消息是如何传递搜索引擎,又是如何从它那边拿到了你想要的结果,抛开那些很复杂的逻辑背后,我们可以来讲一讲互联网的基石 —— 网络协议。

什么是网络协议

网络协议,也被成为传输协议,这里我们指的是,在任何物理介质中允许两个或多个在传输系统中的终端之间传播信息的系统标准,也是指计算机通信或网路设备的共同语言,举一个现实的例子,一名土生土长的亚马逊丛林部落人 (MachineA) 和一名阿拉伯人 (MachineB) 之间肯定无法直接交流,正常来说,直接加配一个翻译即可,但是对于计算机而言,使用第三方插件 (用翻译) 远远没有统一标准化来的方便 (两个人都学同一门语言),只要将这个 “语言” 规范化,每台终端设备都将其内置,万物互联的网络就诞生了。

协议的运用

不只是网络,其实生活方方面面我们也能看的协议的存在,例如 WIFI6 协议、USB3.2 协议等等,注意这些属于公共协议,背后有相应的联盟在制定标准更新,与之相对的还有私有协议,例如各大手机厂家的快充协议,如果不是使用原厂充电头,协议无法被匹配,就无法使用快充模式,在后面我们也可以自己来写一套简单的私有协议

协议遵循的规范

在了解具体的协议详情之前,我们需要抛开细节,来着重讲解一下现行互联网络的参考模型,这是一个很抽象的结构,你可以将其具象化为一个 Request (请求) 所需要经过的流程,例如地址栏输入 [baidu.com](http://baidu.com) 后敲击回车,你可以简单理解为向百度服务器发起了一次 Request,正确理解这些术语可以让我们后面写代码的时候不至于两眼一抹黑。

每一本讲解网络原理的书本上,第一章或者第二章肯定会是 OSI 七层模型,足以说明它的重要性和必要,开放式系统互联模型 (Open System Interconnection Model, OSI) 是由国际标准化组织 ISO 提出,ISO的成员由来自世界上100多个国家的国家标准化团体组成,代表中国参加 ISO 的国家机构是中国国家技术监督局(CSBTS),可以看看牛奶包装上,是不是必须要经过 ISO 认证才能上市,所以很好记 —— ISO 组织提出了 OSI 模型。

OSI 模型将通信系统中的数据流划分为七个层,从分布式应用程序数据的最高层表示到跨通信介质传输数据的物理实现。每个中间层为其上一层提供功能,其自身功能则由其下一层提供,功能的类别通过标准的通信协议在软件中实现。先快速过一遍一次请求中,所需要停留的每一层的作用:

  • 首先我们会发起一个 WebRequest 给 Baidu Server (百度服务器) —— 应用层

  • Request 数据如果包含图片等内容,会被压缩打包 —— 表示层

  • 计算机选择如何建立连接以及附属工作,例如是否需要优先传递这个 Request —— 会话层

  • 计算机在你和 Server 之间寻找出一条最优路由 —— 网络层

阅读更多