<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; K8S</title>
	<atom:link href="https://blog.gmem.cc/tag/k8s/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Thu, 16 Apr 2026 07:10:45 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager</title>
		<link>https://blog.gmem.cc/investigating-solving-issue-failed-certificate-request-zerossl-cert-manager</link>
		<comments>https://blog.gmem.cc/investigating-solving-issue-failed-certificate-request-zerossl-cert-manager#comments</comments>
		<pubDate>Mon, 14 Oct 2024 06:45:45 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Cloud]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=40243</guid>
		<description><![CDATA[<p>In this blog post, I will walk through my journey investigating and resolving an issue where my certificate request from ZeroSSL, using <a class="read-more" href="https://blog.gmem.cc/investigating-solving-issue-failed-certificate-request-zerossl-cert-manager">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/investigating-solving-issue-failed-certificate-request-zerossl-cert-manager">Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><p>In this blog post, I will walk through my journey investigating and resolving an issue where my certificate request from ZeroSSL, using Cert-Manager, remained in a "not ready" state for over two days. I'll cover the tools involved, provide background information, and show how I eventually identified and fixed the problem.</p>
<div class="blog_h1"><span class="graybg">Background on ACME</span></div>
<div class="blog_h2"><span class="graybg">ACME</span></div>
<p>ACME (Automatic Certificate Management Environment) is a protocol developed by the Internet Security Research Group (ISRG) to automate the process of obtaining and managing SSL/TLS certificates from Certificate Authorities (CAs). It simplifies the traditionally manual steps involved in certificate issuance by using an automated process. ACME is widely used by services like Let’s Encrypt and ZeroSSL to secure websites with HTTPS.</p>
<p>The ACME protocol automates interactions between a client (such as Cert-Manager or Certbot) and a CA, allowing the client to request, renew, and manage certificates without human intervention. ACME operates through a series of challenges that prove ownership of the domain for which a certificate is requested. Once the ownership is verified, the CA can issue the certificate.</p>
<div class="blog_h3"><span class="graybg">HTTP-01 Challenge</span></div>
<p>In this challenge, the client proves ownership of a domain by hosting a specific file at a designated path (e.g., http://gmem.cc/.well-known/acme-challenge/). The CA attempts to retrieve this file, and if successful, the challenge is validated. This challenge is commonly used for publicly accessible web servers.</p>
<div class="blog_h3"><span class="graybg">DNS-01 Challenge</span></div>
<p>In this challenge, the client proves domain ownership by creating a special DNS TXT record for the domain. The CA checks the DNS record to confirm ownership. DNS-01 is typically used for wildcard certificates or when the server is not publicly accessible because it doesn’t rely on serving files over HTTP.</p>
<div class="blog_h3"><span class="graybg">TLS-ALPN-01 Challenge</span></div>
<p>This challenge requires the client to prove control of a domain by configuring a TLS server with a special certificate during the ACME validation process. The CA then connects to the server via TLS and checks the certificate. This challenge is less common and usually used in specialized environments.</p>
<div class="blog_h1"><span class="graybg">Background on Cert-Manager</span></div>
<p>Cert-Manager is an open-source Kubernetes add-on that automates the management, issuance, and renewal of certificates within Kubernetes clusters. It integrates with various Certificate Authorities (CAs) and protocols, including ACME (used by providers like Let’s Encrypt and ZeroSSL). Cert-Manager is widely used to ensure that certificates remain valid and secure without manual intervention.</p>
<div class="blog_h2"><span class="graybg">Cert-Manager Components</span></div>
<p>When deploying Cert-Manager in Kubernetes, several key components work together to handle certificate management.</p>
<div class="blog_h3"><span class="graybg">cert-manager</span></div>
<p>The core component of the Cert-Manager system, responsible for managing the lifecycle of certificates and interacting with Issuers (such as ACME servers like Let’s Encrypt or ZeroSSL). It runs as a Kubernetes controller and is responsible for:</p>
<ol>
<li>Watching Certificate, CertificateRequest, Issuer, ClusterIssuer, Order, and Challenge resources.</li>
<li>Requesting certificates from CAs.</li>
<li>Automatically renewing certificates before expiration.</li>
<li>Handling the interactions with external CAs (via ACME, Vault, Venafi, etc.).</li>
</ol>
<p>This component performs the actual management of certificates, from creation to renewal, ensuring that the requested certificates are stored securely as Kubernetes Secrets.</p>
<div class="blog_h3"><span class="graybg">cert-manager-cainjector</span></div>
<p>The CA Injector is an additional component that works alongside Cert-Manager to inject CA data into other Kubernetes resources. It primarily operates on Kubernetes ValidatingWebhookConfiguration and MutatingWebhookConfiguration resources, injecting certificates into them automatically. This is necessary for:</p>
<ol>
<li>Mutating admission controllers that require TLS certificates for secure communication.</li>
</ol>
<ol>
<li>Ensuring that Kubernetes components relying on CA certificates have up-to-date CA data.</li>
</ol>
<p>The cainjector is critical in environments where certain Kubernetes components (e.g., webhooks) require their certificates to be signed by a trusted CA.</p>
<div class="blog_h3"><span class="graybg">cert-manager-webhook</span></div>
<p>The Webhook component provides an admission controller that validates Cert-Manager resources like Certificate, Issuer, ClusterIssuer, and CertificateRequest upon creation or update. It ensures that the Cert-Manager resources are correctly configured by:</p>
<ol>
<li>Validating resources before they are accepted into the Kubernetes API (syntax and structure).</li>
</ol>
<ol>
<li>Mutating resources to provide defaults (for example, setting default values in a Certificate resource).</li>
</ol>
<ol>
<li>Providing a layer of security and correctness by ensuring invalid configurations are caught early.</li>
</ol>
<p>The webhook helps catch configuration issues early, improving the reliability of certificate management workflows.</p>
<div class="blog_h3"><span class="graybg">cert-manager-webhook-dnspod</span></div>
<p>This webhook, which is maintained by the <a href="https://github.com/imroc/cert-manager-webhook-dnspod">community</a>, specifically handles DNS-01 challenges for domains hosted in Tencent Cloud’s DNSPod. When Cert-Manager requests a certificate using the DNS-01 challenge, it needs to create a DNS TXT record in the domain's DNS zone. cert-manager-webhook-dnspod facilitates this by interacting with the DNSPod API to manage DNS records.</p>
<p>Cert-Manager invokes this webhook when it needs to solve a DNS-01 challenge using DNSPod as the DNS provider. The webhook receives instructions from Cert-Manager, communicates with the DNSPod API to create or delete DNS TXT records, and reports back to Cert-Manager when the challenge is complete.</p>
<p>In this post the webhook was created with the following manifest:</p>
<pre class="crayon-plain-tag">apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cert-manager-webhook-dnspod
  namespace: argocd
spec:
  destination:
    namespace: cert-manager
    server: https://kubernetes.default.svc
  project: default
  source:
    repoURL: https://github.com/imroc/cert-manager-webhook-dnspod
    targetRevision: master
    path: charts/cert-manager-webhook-dnspod
    helm:
      releaseName: cert-manager-webhook-dnspod
      values: |
        groupName: acme.dnspod.tencent.com
        clusterIssuer:
          name: letsencrypt-issuer
          secretId: DNSPOD_SECRETID
          secretKey: DNSPOD_SECRETKEY
          email: gmem@me.com
  syncPolicy:
    syncOptions:
      - CreateNamespace=true
    automated:
      prune: true
      selfHeal: true
 </pre>
<div class="blog_h2"><span class="graybg">Cert-Manager CRDs</span></div>
<p>Cert-Manager relies on several types of custom resources that work together to manage the certificate lifecycle:</p>
<ol style="list-style-type: undefined;">
<li>Issuer/ClusterIssuer:
<ol>
<li>An Issuer or ClusterIssuer is a Cert-Manager resource that defines how certificates should be requested from a Certificate Authority (CA). The difference between them is that an Issuer is scoped to a single namespace, whereas a ClusterIssuer can be used cluster-wide.</li>
<li>Issuers can be configured to use different CA backends, such as ACME, Vault, or self-signed certificates.</li>
</ol>
</li>
<li>Certificate: The Certificate resource defines which certificates should be issued and managed by Cert-Manager. It specifies details such as the domain names, issuer to use, renewal period, and where to store the certificate (usually in a Kubernetes secret).</li>
<li>CertificateRequest: When a Certificate resource is created, Cert-Manager generates a CertificateRequest. This resource represents the actual request to the Issuer for a certificate. Cert-Manager manages these requests and handles the approval and signing process.</li>
<li>Order: When Cert-Manager requests a certificate from an ACME-based Issuer, it creates an Order resource to track the status of the certificate issuance. The Order keeps track of challenges and interactions with the ACME server.</li>
<li>Challenge: A Challenge resource represents the ACME challenge issued by the CA (such as DNS-01 or HTTP-01). The challenge proves domain ownership by requiring the client (Cert-Manager) to respond to a domain validation request from the CA.</li>
</ol>
<div class="blog_h2"><span class="graybg">Cert-Manager  Challenge Workflows</span></div>
<div class="blog_h3"><span class="graybg">HTTP-01 Challenge<br /></span></div>
<p>The HTTP-01 challenge is used when the domain is publicly accessible over HTTP. The CA validates ownership of the domain by requesting a specific file over HTTP. Cert-Manager sets up the challenge response using a Kubernetes Ingress resource.</p>
<ol>
<li>Create an Issuer or ClusterIssuer: The Issuer defines the connection to the CA (such as Let’s Encrypt or ZeroSSL) and specifies that ACME should be used with the HTTP-01 challenge.</li>
<li>Create a Certificate Resource: A Certificate resource is created that specifies the domain names for which a certificate is needed and the Issuer to use.</li>
<li>Cert-Manager Creates a CertificateRequest: Cert-Manager generates a CertificateRequest based on the Certificate resource.</li>
<li>Cert-Manager Creates an Order: Cert-Manager creates an Order resource to track the status of the certificate request with the ACME server.</li>
<li>Cert-Manager Creates an HTTP-01 Challenge: An HTTP-01 challenge is created, and Cert-Manager configures an Ingress resource to serve the challenge response at the path /.well-known/acme-challenge/.</li>
<li>ACME Server Attempts to Validate: The CA attempts to access the challenge file via the HTTP URL (e.g., http://example.com/.well-known/acme-challenge/&lt;token&gt;). If successful, the challenge is validated.</li>
<li>Certificate Issued: Once the challenge is validated, the CA issues the certificate, and Cert-Manager stores it in the specified Kubernetes Secret.</li>
</ol>
<div class="blog_h3"><span class="graybg">DNS-01 Challenge </span></div>
<p>The DNS-01 challenge is used when domain ownership must be validated via DNS records. This method is often preferred for wildcard certificates or domains that are not publicly accessible over HTTP.</p>
<ol>
<li>Create an Issuer or ClusterIssuer: The Issuer specifies that ACME should be used with the DNS-01 challenge and includes configuration for interacting with the DNS provider (e.g., AWS Route53, Cloudflare, or a custom webhook like dnspod).</li>
<li>Create a Certificate Resource: A Certificate resource is created that defines the domains for which the certificate is needed and the Issuer to use.</li>
<li>Cert-Manager Creates a CertificateRequest: Cert-Manager generates a CertificateRequest based on the Certificate resource.</li>
<li>Cert-Manager Creates an Order: Cert-Manager creates an Order resource to track the status of the certificate request with the ACME server.</li>
<li>Cert-Manager Creates a DNS-01 Challenge: A DNS-01 challenge is created, and Cert-Manager interacts with the configured DNS provider to automatically create a special TXT record for the domain (e.g., _acme-challenge.example.com).</li>
<li>ACME Server Attempts to Validate: The CA checks for the presence of the _acme-challenge.example.com TXT record in the domain’s DNS records. If the correct value is found, the challenge is validated.</li>
<li>Certificate Issued: Once the DNS challenge is validated, the CA issues the certificate, and Cert-Manager stores it in the specified Kubernetes Secret.</li>
</ol>
<div class="blog_h1"><span class="graybg">The Problem, Investigation and Fix<br /></span></div>
<div class="blog_h2"><span class="graybg">Problem Statement </span></div>
<p>I created a ClusterIssuer and a Certificate for the wildcard domain *.gmem.cc. However, after waiting for more than two days, the certificate status still showed as not ready, with the following condition in the certificate's status:</p>
<pre class="crayon-plain-tag">status:
  conditions:
    - lastTransitionTime: "2024-10-14T06:02:10Z"
      message: Issuing certificate as Secret does not exist
      observedGeneration: 1
      reason: DoesNotExist
      status: "False"
      type: Ready</pre>
<p>I checked on DNSPod and found a <pre class="crayon-plain-tag">TXT</pre> record <pre class="crayon-plain-tag">_acme-challenge.gmem.cc</pre> with the TTL set to 600 seconds.</p>
<div class="blog_h2"><span class="graybg">Initial Investigation</span></div>
<p>To diagnose the issue, I checked the status of the related Cert-Manager resources: <pre class="crayon-plain-tag">CertificateRequest</pre>, <pre class="crayon-plain-tag">Order</pre>, and <pre class="crayon-plain-tag">Challenge</pre>. These are the key resources that Cert-Manager uses to interact with ACME and handle certificate issuance. Here’s an overview of my findings:</p>
<p>CertificateRequest:</p>
<pre class="crayon-plain-tag">status:
  conditions:
    - lastTransitionTime: "2024-10-14T06:02:14Z"
      message: Certificate request has been approved by cert-manager.io
      reason: cert-manager.io
      status: "True"
      type: Approved
    - lastTransitionTime: "2024-10-14T06:02:14Z"
      message: 'Waiting on certificate issuance from order istio-system/wildcard-ssl-5pgsr-3353861729: "pending"'
      reason: Pending
      status: "False"
      type: Ready</pre>
<p>The certificate request was approved, but the system was waiting for the certificate issuance to complete.</p>
<p>Order: </p>
<pre class="crayon-plain-tag">status:
  state: pending
  authorizations:
    - challenges:
        - token: iQMwrfsFRmJ_MytUY3N4NW6QehtTn0-IEvJWAmYEw_k
          type: dns-01
          url: https://acme.zerossl.com/v2/DV90/chall/DDRBMBd9jnJo_W4EcQfSWQ
      wildcard: true</pre>
<p>The order was still in a "pending" state, and the DNS-01 challenge had not been completed yet.</p>
<p>Challenge:</p>
<pre class="crayon-plain-tag">status:
  presented: true
  processing: true
  reason: 'Waiting for DNS-01 challenge propagation: DNS record for "gmem.cc" not yet propagated'
  state: pending</pre>
<p>The challenge was waiting for DNS propagation, but apparently the <pre class="crayon-plain-tag">TXT</pre> record I mentioned above had been created two days ago.</p>
<div class="blog_h2"><span class="graybg">Global DNS Propagration Check</span></div>
<p>Mutifarious reasons can cause delay on DNS propagation, we can check whether TXT record  _acme-challenge.gmem.cc is synchronized all over the world using <a href="https://www.whatsmydns.net/">whatsmydns.net</a>:</p>
<p style="padding-left: 30px;">https://www.whatsmydns.net/#TXT/_acme-challenge.gmem.cc</p>
<p>In this case, the check result was that the record had been fully propagated.</p>
<div class="blog_h2"><span class="graybg">Cert-Manager Logs</span></div>
<p>I then checked the logs for the Cert-Manager pod to see if any errors were being reported. The relevant and repeating error message from the logs was:</p>
<p style="padding-left: 30px;">E1014 05:52:58.647839 1 sync.go:190] "cert-manager/challenges: propagation check failed" err="DNS record for \"gmem.cc\" not yet propagated"</p>
<p>At this point, it seemed that Cert-Manager was unable to see the propagated DNS records, even though I had confirmed their existence.</p>
<p>After enabling verbose logging (--v5 log level) in Cert-Manager, I finally discovered the root cause. The detailed logs revealed that Cert-Manager was checking the DNS record at an intermediate CNAME:</p>
<p style="padding-left: 30px;">I1014 06:20:33.919849 1 dns.go:116] "cert-manager/challenges/Check: checking DNS propagation"<br />I1014 06:20:33.921190 1 wait.go:90] Updating FQDN: _acme-challenge.gmem.cc. with its CNAME: lb-db1nok14-foh4te5vrj0dya3c.clb.sg-tencentclb.com.<br />I1014 06:25:35.886683 1 wait.go:298] Searching fqdn "lb-db1nok14-foh4te5vrj0dya3c.clb.sg-tencentclb.com." using seed nameservers [10.231.18.121:53]<br />I1014 06:25:35.886696 1 wait.go:329] Returning cached zone record "sg-tencentclb.com." for fqdn "lb-db1nok14-foh4te5vrj0dya3c.clb.sg-tencentclb.com."<br />I1014 06:20:33.987786 1 wait.go:141] Looking up TXT records for "lb-db1nok14-foh4te5vrj0dya3c.clb.sg-tencentclb.com."</p>
<p>The challenge was failing because the DNS record for  <pre class="crayon-plain-tag">*.gmem.cc</pre> was a  CNAME pointing to another domain, lb-db1nok14-foh4te5vrj0dya3c.clb.sg-tencentclb.com, which caused Cert-Manager to search for a wrong TXT record.</p>
<div class="blog_h2"><span class="graybg">The Fix</span></div>
<p>The solution was to<span style="background-color: #c0c0c0;"> remove the wildcard record *.gmem.cc which was CNAMEed to Tencent Cloud Loadbalancer address.</span> After a few minutes, the DNS cache was invalidated and Cert-Manager finally logged something different:</p>
<p style="padding-left: 30px;">I1014 06:25:45.974354 1 wait.go:298] Searching fqdn "_acme-challenge.gmem.cc." using seed nameservers [10.231.18.121:53]<br />I1014 06:25:46.453434 1 wait.go:383] Returning discovered zone record "gmem.cc." for fqdn "_acme-challenge.gmem.cc."<br />I1014 06:25:46.454660 1 wait.go:316] Returning authoritative nameservers [c.dnspod.com., a.dnspod.com., b.dnspod.com.]<br />I1014 06:25:46.462705 1 wait.go:141] Looking up TXT records for "_acme-challenge.gmem.cc."</p>
<p>indicating that the correct TXT record was found. And from the subsequent logs some working detailed of Cert-Manager was revealed:</p>
<p style="padding-left: 30px;">I1014 06:25:47.050500 1 dns.go:128] "cert-manager/challenges/Check: waiting DNS record TTL to allow the DNS01 record to propagate for domain" resource_name="wildcard-ssl-5pgsr-3353861729-3026093647" resource_namespace="istio-system" resource_kind="Challenge" resource_version="v1" dnsName="gmem.cc" type="DNS-01" resource_name="wildcard-ssl-5pgsr-3353861729-3026093647" resource_namespace="istio-system" resource_kind="Challenge" resource_version="v1" domain="gmem.cc" ttl=60 fqdn="_acme-challenge.gmem.cc."</p>
<p style="padding-left: 60px;">This line indicated that after Cert-Manager validated the TXT record locally, it would wait for TTL ( 60 here ) seconds, just in case that Zero SSL server hadn't been able to see te record.</p>
<p style="padding-left: 30px;">014 06:26:47.051650 1 sync.go:359] "cert-manager/challenges/acceptChallenge: accepting challenge with ACME server" resource_name="wildcard-ssl-5pgsr-3353861729-3026093647" resource_namespace="istio-system" resource_kind="Challenge" resource_version="v1" dnsName="gmem.cc" type="DNS-01"<br />I1014 06:26:47.051665 1 logger.go:81] "cert-manager/acme-middleware: Calling Accept"<br />I1014 06:26:49.708221 1 sync.go:376] "cert-manager/challenges/acceptChallenge: waiting for authorization for domain" resource_name="wildcard-ssl-5pgsr-3353861729-3026093647" resource_namespace="istio-system" resource_kind="Challenge" resource_version="v1" dnsName="gmem.cc" type="DNS-01"<br />I1014 06:26:49.708250 1 logger.go:99] "cert-manager/acme-middleware: Calling WaitAuthorization"<br />I1014 06:26:50.194610 1 logs.go:199] "cert-manager/controller: Event(v1.ObjectReference{Kind:\"Challenge\", Namespace:\"istio-system\", Name:\"wildcard-ssl-5pgsr-3353861729-3026093647\", UID:\"a4b2f2d7-6185-4072-92f0-b7a89418cdb1\", APIVersion:\"acme.cert-manager.io/v1\", ResourceVersion:\"463570645\", FieldPath:\"\"}): type: 'Normal' reason: 'DomainVerified' Domain \"gmem.cc\" verified with \"DNS-01\" validation"</p>
<p style="padding-left: 60px;">After the wait, Cert-Manager called Zero SSL server for Accept and WaitAuthorization operation and the server verified that we were the owner of the domain name.</p>
<div class="blog_h1"><span class="graybg">Conclusion</span></div>
<p>In this case, the root cause of the certificate issuance failure was a CNAME record interfering with Cert-Manager's DNS-01 challenge. It had nothing to do with the ACME server but was linked to Cert-Manager's internal implementation.</p>
<p>To aviod similar issues in the future, we need to stop using wildcard domain records.</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/investigating-solving-issue-failed-certificate-request-zerossl-cert-manager">Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/investigating-solving-issue-failed-certificate-request-zerossl-cert-manager/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>K8S集群跨云迁移</title>
		<link>https://blog.gmem.cc/k8s-migration</link>
		<comments>https://blog.gmem.cc/k8s-migration#comments</comments>
		<pubDate>Tue, 27 Dec 2022 11:37:50 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=39115</guid>
		<description><![CDATA[<p>要将K8S集群从一个云服务商迁移到另外一个，需要解决以下问题： 各种K8S资源的迁移 工作负载所挂载的数据卷的迁移 工作负载所使用的镜像的迁移 K8S资源、数据卷的迁移，可以考虑基于Velero项目。镜像仓库的迁移较为简单，可供选择的开源工具包括阿里云的image-syncer、腾讯云的image-transfer。 Velero 简介 Velero是一个专注于K8S集群备份与恢复的开源项目，通过备份源集群，并在目标集群进行恢复，即可完成集群的跨云迁移。 Velero由一组运行在被备份/恢复的K8S集群中的服务，外加一个CLI客户端组成。服务端包含若干控制器，这些控制器监听备份、恢复相关的自定义资源（CRD）并进行处理。CLI主要是提供了创建、修改、删除自定义资源的快捷方式，让用户不必编写复杂的YAML。 主要新特性 在Kubernetes故障检测和自愈一文中，我们对Velero做过调研，这三年多时间里，Velero新增加的特性包括： 多读写的持久卷不会被重复备份 云服务商插件从Velero代码库独立出 基于Restic的持久卷备份总是增量的，即使Pod被调度走 克隆命名空间（将备份恢复到另外一个命名空间）时自动克隆相关的持久卷 支持处理基于CSI驱动创建的持久卷，目前支持的厂商包括AWS、Azure、GCP 支持报告备份、恢复的进度 支持备份资源的所有API版本 支持自动基于Restic进行卷备份（--default-volumes-to-restic） 选项restoreStatus，用于定制那些资源的状态会被恢复 --existing-resource-policy用于修改默认的恢复策略。默认策略时当资源存在时不覆盖（除了ServiceAccount），该选项设置为update，则会更新已存在的资源 从1.10开始，支持Kopia，作为Restic的备选。Kopia在备份时消耗的时间较少、在备份数据量很大或者文件数量很多时，Kopia常常有更好的性能 <a class="read-more" href="https://blog.gmem.cc/k8s-migration">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/k8s-migration">K8S集群跨云迁移</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><p>要将K8S集群从一个云服务商迁移到另外一个，需要解决以下问题：</p>
<ol style="list-style-type: undefined;">
<li>各种K8S资源的迁移</li>
<li>工作负载所挂载的数据卷的迁移</li>
<li>工作负载所使用的镜像的迁移</li>
</ol>
<p>K8S资源、数据卷的迁移，可以考虑基于<a href="https://velero.io/">Velero项目</a>。镜像仓库的迁移较为简单，可供选择的开源工具包括阿里云的<a href="https://github.com/AliyunContainerService/image-syncer">image-syncer</a>、腾讯云的<a href="https://github.com/tkestack/image-transfer">image-transfer</a>。</p>
<div class="blog_h1"><span class="graybg">Velero</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>Velero是一个专注于K8S集群备份与恢复的开源项目，通过备份源集群，并在目标集群进行恢复，即可完成集群的跨云迁移。</p>
<p>Velero由一组运行在被备份/恢复的K8S集群中的服务，外加一个CLI客户端组成。服务端包含若干控制器，这些控制器监听备份、恢复相关的自定义资源（CRD）并进行处理。CLI主要是提供了创建、修改、删除自定义资源的快捷方式，让用户不必编写复杂的YAML。</p>
<div class="blog_h3"><span class="graybg">主要新特性</span></div>
<p>在<a href="/problem-detection-and-auto-repairing-in-k8s#velero">Kubernetes故障检测和自愈</a>一文中，我们对Velero做过调研，这三年多时间里，Velero新增加的特性包括：</p>
<ol style="list-style-type: undefined;">
<li>多读写的持久卷不会被重复备份</li>
<li>云服务商插件从Velero代码库独立出</li>
<li>基于Restic的持久卷备份总是增量的，即使Pod被调度走</li>
<li>克隆命名空间（将备份恢复到另外一个命名空间）时自动克隆相关的持久卷</li>
<li>支持处理基于CSI驱动创建的持久卷，目前支持的厂商包括AWS、Azure、GCP</li>
<li>支持报告备份、恢复的进度</li>
<li>支持备份资源的所有API版本</li>
<li>支持自动基于Restic进行卷备份（--default-volumes-to-restic）</li>
<li>选项restoreStatus，用于定制那些资源的状态会被恢复</li>
<li>--existing-resource-policy用于修改默认的恢复策略。默认策略时当资源存在时不覆盖（除了ServiceAccount），该选项设置为update，则会更新已存在的资源</li>
<li>从1.10开始，支持Kopia，作为Restic的备选。Kopia在备份时消耗的时间较少、在备份数据量很大或者文件数量很多时，Kopia常常有更好的性能</li>
</ol>
<div class="blog_h3"><span class="graybg">备份流程</span></div>
<p>备份分为两种方式：按需备份、周期性备份。它们都是收集（支持过滤）K8S资源并打包上传到存储后端（例如云服务商的对象存储）。</p>
<p>备份的典型流程如下：</p>
<ol>
<li>用户通过命令<pre class="crayon-plain-tag">velero backup create</pre>创建<pre class="crayon-plain-tag">Backup</pre>资源</li>
<li>控制器<pre class="crayon-plain-tag">BackupController</pre>监听到新创建的Backup资源，并进行校验</li>
<li>校验成功后BackupController开始执行备份操作。默认情况下会为所有持久卷创建快照，使用命令行选项<pre class="crayon-plain-tag">--snapshot-volumes=false</pre>改变此默认行为</li>
<li>BackupController调用对象存储，上传备份文件</li>
</ol>
<p>备份资源的时候，Velero使用<span style="background-color: #c0c0c0;">首选API版本（preferred version）获取和保存资源</span>。举例来说，源API Server中teleport组具有v1alpha1、v1两个版本，其中v1是首选版本，那么Velero备份的时候将使用v1格式存储资源。在恢复的时候，目标集群必须支持teleport/v1版本（但不必是首选版本），这意味着，<span style="background-color: #c0c0c0;">恢复到和源的版本不同的K8S集群可能会出错，因为API版本会有新增或删除</span>。</p>
<p>备份的时候，可以通过--ttl来指定备份的存留时间。当存留时间到达后，备份中的K8S资源、备份文件、快照、关联的Restore，均被删除。如果删除失败Velero会给Backup对象添加velero.io/gc-failure=REASON标签。</p>
<p>需要注意：<span style="background-color: #c0c0c0;">Velero基于快照的卷备份，在跨云迁移这一场景下，是没有意义的，不可能把云A生成的快照拿到云T上恢复</span>。</p>
<div class="blog_h3"><span class="graybg">恢复流程</span></div>
<p>恢复是将先前生成的备份（包括K8S资源 + 数据卷），同步到目标K8S集群的过程。<span style="background-color: #c0c0c0;">目标集群可以是源集群自身</span>，可以<span style="background-color: #c0c0c0;">对备份中的资源进行过滤</span>，仅恢复一部分数据。</p>
<p>恢复产生的K8S资源，具有velero.io/restore-name=RESTORE_NAME标签。RESTORE_NAME即Restore资源的名字，默认形式为BACKUP_NAME-TIMESTAMP，TIMESTAMP的默认格式为YYYYMMDDhhmmss。</p>
<p>恢复的典型流程如下：</p>
<ol>
<li>用户通过命令<pre class="crayon-plain-tag">velero restore create</pre>创建<pre class="crayon-plain-tag">Restore</pre>资源</li>
<li>控制器<pre class="crayon-plain-tag">RestoreController</pre>监听到新创建的Restore资源，并进行校验</li>
<li>校验成功后，RestoreController从对象存储中获取备份的信息，对备份的资源进行一些预处理，例如API版本检查，以保证它们能够在新集群中运行</li>
<li>开始逐个资源的进行恢复</li>
</ol>
<p><span style="background-color: #c0c0c0;">默认情况下，Velero不会对目标集群进行任何修改、删除操作</span>，如果某个资源已经存在，它会简单的跳过。设置 --existing-resource-policy=update则Velero会尝试修改已经存在的资源，使其和备份中的同名资源内容匹配。</p>
<div class="blog_h3"><span class="graybg">关于对象存储</span></div>
<p>保存备份信息的<span style="background-color: #c0c0c0;">对象存储，是Velero的单一数据源</span>（source of truth），也就是说：</p>
<ol>
<li>如果对象存储中具有备份信息，但是K8S API资源中没有对应的Backup对象，那么会自动创建这些对象</li>
<li>如果K8S中有Backup对象，但是在对象存储中没有对应信息，那么此Backup会被删除</li>
</ol>
<p>这一特性也让跨云迁移场景获益 —— 源、目标集群不需要任何直接的关联，云服务商的对象存储服务将作为唯一的媒介。</p>
<p>定义对象存储位置的CRD是<pre class="crayon-plain-tag">BackupStorageLocation</pre>，它可以指向一个桶，或者桶中的某个前缀，<span style="background-color: #c0c0c0;">Velero备份的元数据数据存放在其中</span>。<span style="background-color: #c0c0c0;">通过文件系统方式（Restic/Kopia）备份的卷，也存放在桶中</span>。通过快照方式备份的卷的数据，其存储机制是云服务商私有的，不会存放到桶中。</p>
<p>每个Backup可以使用一个BackupStorageLocation。</p>
<div class="blog_h3"><span class="graybg">关于快照</span></div>
<p>存储卷快照相关的信息存储存储在<pre class="crayon-plain-tag">VolumeSnapshotLocation</pre>中，由于快照实现技术完全取决于云服务商，因此该CRD的包含的字段取决于对应插件。</p>
<p>每个Backup，对于任何一个Volume Provider，可以使用一个VolumeSnapshotLocation。</p>
<div class="blog_h3"><span class="graybg">关于提供者</span></div>
<p>Velero提供了一种插件机制，将存储提供者从Velero核心中独立出来。</p>
<div class="blog_h3"><span class="graybg">钩子机制</span></div>
<p>Velero提供了若干钩子来扩展标准的备份/恢复流程。</p>
<p>备份钩子在备份流程中执行。例如你可以利用备份钩子，在创建快照前，通知数据库刷空内存缓冲。</p>
<p>恢复钩子在恢复流程中执行。例如你可以在应用启动前，通过钩子进行某种初始化操作。</p>
<div class="blog_h2"><span class="graybg">安装</span></div>
<div class="blog_h3"><span class="graybg">安装CLI</span></div>
<p>目前最新稳定版本是<a href="https://github.com/vmware-tanzu/velero/releases/tag/v1.9.5">1.9.5</a>，点击此连接，选择匹配操作系统的压缩包，下载解压，存放velero到$PATH即可。执行下面的命令配置自动提示：</p>
<pre class="crayon-plain-tag">echo 'source &lt;(velero completion bash)' &gt;&gt;~/.bashrc </pre>
<p>下面的命令实例，展示了如何进行客户端设置：</p>
<pre class="crayon-plain-tag"># 启用客户端特性
velero client config set features=EnableCSI
# 关闭彩色字符输出
velero client config set colorized=false</pre>
<div class="blog_h3"><span class="graybg">安装服务端</span></div>
<p>使用CLI安装：</p>
<pre class="crayon-plain-tag">velero install 
    # 指定命名空间
    --namespace=teleport-system
    # 支持基于文件系统的卷备份（FSB）
    --use-node-agent
    # 默认启用基于文件系统的卷备份，来备份所有Pod卷。缺省行为是必须指定注解才备份
    # backup.velero.io/backup-volumes=YOUR_VOLUME_NAME_1,YOUR_VOLUME_NAME_2,...
    # 如果准备使用FSB作为唯一的卷备份机制，可以开启
    # 启用该命令后，如果还想在某次备份时，使用快照备份，使用命令 backup create --snapshot-volumes
    # 也可以在备份时使用backup create --default-volumes-to-fs-backup，而不是在安装阶段全局性设置
    --default-volumes-to-fs-backup
    # 特性开关，这些开关也会被传递给node-agent
    --features=EnableCSI,EnableAPIGroupVersions
    # 设置资源请求/限制。默认值对于1000-资源、100GB-卷的场景足够。如果使用FSB，则可能需要增加资源请求/限制
    --velero-pod-cpu-request
    --velero-pod-mem-request
    --velero-pod-cpu-limit
    --velero-pod-mem-limit
    --node-agent-pod-cpu-request
    --node-agent-pod-mem-request
    --node-agent-pod-cpu-limit
    --node-agent-pod-mem-limit
    # 在安装阶段，你可以指定一个BackupLocation和SnapshotLocation
    --provider aws
    --bucket backups 
    --secret-file ./aws-iam-creds 
    --backup-location-config region=us-east-2 
    --snapshot-location-config region=us-east-2
    # 如果在安装阶段，不配置BackupLocation，可以不指定--bucket和--provider，同时：
    --no-default-backup-location
    # 仅仅生成YAML
    --dry-run -o yaml</pre>
<p>在安装完毕后，你可以设置默认的 BackupLocation、SnapshotLocation：</p>
<pre class="crayon-plain-tag">velero backup-location create backups-primary \
    --provider aws \
    --bucket velero-backups \
    --config region=us-east-1 \
    --default

# SnapshotLocation需要为每一个volume snapshot provider设置
velero server --default-volume-snapshot-locations="PROVIDER-NAME:LOCATION-NAME,PROVIDER2-NAME:LOCATION2-NAME"</pre>
<p>在安装完毕后，可以添加额外的volume snapshot provider：</p>
<pre class="crayon-plain-tag">velero plugin add registry/image:version
# 为此volume snapshot provider配置SnapshotLocation
velero snapshot-location create NAME -provider PROVIDER-NAME [--config PROVIDER-CONFIG]</pre>
<div class="blog_h2"><span class="graybg">测试跨云迁移</span></div>
<p>我们分别在阿里云、腾讯云上创建一个K8S集群，分别作为源、目标集群，并尝试利用Velero将工作负载从源迁移到目标。</p>
<div class="blog_h3"><span class="graybg">创建集群</span></div>
<p>到云服务商的控制台创建，这里不赘述具体步骤。</p>
<div class="blog_h3"><span class="graybg">无状态工作负载迁移</span></div>
<p>目前K8S集群的主流用法是运行无状态的工作负载。数据库等有状态的基础服务，大多数用户选择使用云平台提供的PaaS产品。这一现状实质上让K8S的迁移变得简单，因为基本不需要考虑数据卷的问题。</p>
<p>这里以一个Nginx的Deployment + Service为例，测试Velero的备份、恢复特性。首先在源集群创建相应K8S资源：</p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:1.7.9
        name: nginx
        ports:
        - containerPort: 80

---

apiVersion: v1
kind: Service
metadata:
  labels:
    app: nginx
  name: nginx
spec:
  ports:
  - port: 80
    targetPort: 80
  selector:
    app: nginx
  type: LoadBalancer</pre>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/k8s-migration">K8S集群跨云迁移</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/k8s-migration/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>编写Kubernetes风格的APIServer</title>
		<link>https://blog.gmem.cc/kubernetes-style-apiserver</link>
		<comments>https://blog.gmem.cc/kubernetes-style-apiserver#comments</comments>
		<pubDate>Fri, 20 Aug 2021 07:33:34 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=38317</guid>
		<description><![CDATA[<p>背景 前段时间接到一个需求做一个工具，工具将在K8S中运行。需求很适合用控制器模式实现，很自然的就基于kubebuilder进行开发了。但是和K8S环境提供方沟通时发现，他们不允许工作负载调用控制平面的接口，这该怎么办呢。 最快速的解决方案是，自己运行一套kube-apiserver + etcd。但是这对我们来说太重了，kube-apiserver很多我们不需要的特性占用了过多资源，因此这里想寻找一个更轻量的方案。 apiserver库 kubernetes/apiserver同步自kubernertes主代码树的taging/src/k8s.io/apiserver目录，它提供了创建K8S风格的API Server所需要的库。包括kube-apiserver、kube-aggregator、service-catalog在内的很多项目都依赖此库。 apiserver库的目的主要是用来构建API Aggregation中的Extension API Server。它提供的特性包括： 将authn/authz委托给主kube-apiserver 支持kuebctl兼容的API发现 支持admisson control链 支持版本化的API类型 K8S提供了一个样例kubernetes/sample-apiserver，但是这个例子依赖于主kube-apiserver。即使不使用authn/authz或API聚合，也是如此。你需要通过--kubeconfig来指向一个主kube-apiserver，样例中的SharedInformer依赖于会连接到主kube-apiserver来访问K8S资源。 sample-apiserver分析 显然我们是不能对主kube-apiserver有任何依赖的，这里分析一下sample-apiserver的代码，看看如何进行改动。 入口点 [crayon-69e09a47aa9c8791600601/] <a class="read-more" href="https://blog.gmem.cc/kubernetes-style-apiserver">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/kubernetes-style-apiserver">编写Kubernetes风格的APIServer</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">背景</span></div>
<p>前段时间接到一个需求做一个工具，工具将在K8S中运行。需求很适合用控制器模式实现，很自然的就基于kubebuilder进行开发了。但是和K8S环境提供方沟通时发现，他们不允许工作负载调用控制平面的接口，这该怎么办呢。</p>
<p>最快速的解决方案是，自己运行一套kube-apiserver + etcd。但是这对我们来说太重了，kube-apiserver很多我们不需要的特性占用了过多资源，因此这里想寻找一个更轻量的方案。</p>
<div class="blog_h1"><span class="graybg">apiserver库</span></div>
<p><a href="https://github.com/kubernetes/apiserver">kubernetes/apiserver</a>同步自kubernertes主代码树的taging/src/k8s.io/apiserver目录，它提供了创建K8S风格的API Server所需要的库。包括kube-apiserver、kube-aggregator、service-catalog在内的很多项目都依赖此库。</p>
<p>apiserver库的目的主要是用来构建API Aggregation中的Extension API Server。它提供的特性包括：</p>
<ol>
<li>将authn/authz委托给主kube-apiserver</li>
<li>支持kuebctl兼容的API发现</li>
<li>支持admisson control链</li>
<li>支持版本化的API类型</li>
</ol>
<p>K8S提供了一个样例<a href="https://github.com/kubernetes/sample-apiserver">kubernetes/sample-apiserver</a>，但是这个例子依赖于主kube-apiserver。即使不使用authn/authz或API聚合，也是如此。你需要通过--kubeconfig来指向一个主kube-apiserver，样例中的SharedInformer依赖于会连接到主kube-apiserver来访问K8S资源。</p>
<div class="blog_h2"><span class="graybg">sample-apiserver分析</span></div>
<p>显然我们是不能对主kube-apiserver有任何依赖的，这里分析一下sample-apiserver的代码，看看如何进行改动。</p>
<div class="blog_h3"><span class="graybg">入口点</span></div>
<pre class="crayon-plain-tag">func main() {
	logs.InitLogs()
	defer logs.FlushLogs()

	stopCh := genericapiserver.SetupSignalHandler()
	// 初始化服务器选项
	options := server.NewWardleServerOptions(os.Stdout, os.Stderr)
	// 启动服务器
	cmd := server.NewCommandStartWardleServer(options, stopCh)
	cmd.Flags().AddGoFlagSet(flag.CommandLine)
	if err := cmd.Execute(); err != nil {
		klog.Fatal(err)
	}
}</pre>
<div class="blog_h3"><span class="graybg">服务器选项 </span></div>
<pre class="crayon-plain-tag">type WardleServerOptions struct {
	RecommendedOptions *genericoptions.RecommendedOptions

	SharedInformerFactory informers.SharedInformerFactory
	StdOut                io.Writer
	StdErr                io.Writer
}

func NewWardleServerOptions(out, errOut io.Writer) *WardleServerOptions {
	o := &amp;WardleServerOptions{
		RecommendedOptions: genericoptions.NewRecommendedOptions(
			// 数据默认存放在Etcd的/registry/wardle.example.com目录下
			defaultEtcdPathPrefix,
			// 指定wardle.example.com/v1alpha1使用遗留编解码器
			apiserver.Codecs.LegacyCodec(v1alpha1.SchemeGroupVersion),
			// API Server的进程信息
			genericoptions.NewProcessInfo("wardle-apiserver", "wardle"),
		),

		StdOut: out,
		StdErr: errOut,
	}
	// wardle.example.com/v1alpha1中的所有对象存储到Etcd
	o.RecommendedOptions.Etcd.StorageConfig.EncodeVersioner = 
		runtime.NewMultiGroupVersioner(v1alpha1.SchemeGroupVersion, 
			schema.GroupKind{Group: v1alpha1.GroupName})
	return o
}</pre>
<p>可以看到，选项的核心是genericoptions.RecommendedOptions，顾名思义，它用于提供运行apiserver所需的“推荐”选项：</p>
<pre class="crayon-plain-tag">type RecommendedOptions struct {
	// Etcd相关的配置
	Etcd           *EtcdOptions
	// HTTPS相关选项，包括监听地址、证书等配置。还负责创建并设置Lookback专用的rest.Config
	SecureServing  *SecureServingOptionsWithLoopback
	// authn选项
	Authentication *DelegatingAuthenticationOptions
	// authz选项
	Authorization  *DelegatingAuthorizationOptions
	// 审计选项
	Audit          *AuditOptions
	// 用于启用剖析、竞态条件剖析
	Features       *FeatureOptions
	// 核心API选项，指定主kube-apiserver配置文件位置
	CoreAPI        *CoreAPIOptions

	// 特性开关
	FeatureGate featuregate.FeatureGate
	// 所有以上选项的ApplyTo被调用后，调用下面的函数。返回的PluginInitializer会传递给Admission.ApplyTo
	ExtraAdmissionInitializers func(c *server.RecommendedConfig) ([]admission.PluginInitializer, error)
	Admission                  *AdmissionOptions
	// 提供服务器信息
	ProcessInfo *ProcessInfo
	// Webhook选项
	Webhook     *WebhookOptions
	// 控制服务器的出站流量
	EgressSelector *EgressSelectorOptions
}</pre>
<p>推荐的选项取值，可以由函数genericoptions.NewRecommendedOptions()提供。RecommendedOptions支持通过命令行参数获取选项取值：</p>
<pre class="crayon-plain-tag">func (o *RecommendedOptions) AddFlags(fs *pflag.FlagSet) {}</pre>
<div class="blog_h3"><span class="graybg">准备服务器 </span></div>
<p>服务器实现为cobra.Command命令，首先会将RecommendedOptions绑定到命令行参数。</p>
<pre class="crayon-plain-tag">func NewCommandStartWardleServer(defaults *WardleServerOptions, stopCh &lt;-chan struct{}) *cobra.Command {
	o := *defaults
	cmd := &amp;cobra.Command{
		Short: "Launch a wardle API server",
		Long:  "Launch a wardle API server",
		RunE: func(c *cobra.Command, args []string) error {
			// ...
		},
	}

	flags := cmd.Flags()
	// 将选项添加为命令行标记
	o.RecommendedOptions.AddFlags(flags)
	utilfeature.DefaultMutableFeatureGate.AddFlag(flags)

	return cmd
}</pre>
<p>然后，调用cmd.Execute()，进而调用上面的RunE方法：</p>
<pre class="crayon-plain-tag">if err := o.Complete(); err != nil {
	return err
}
if err := o.Validate(args); err != nil {
	return err
}
if err := o.RunWardleServer(stopCh); err != nil {
	return err
}
return nil</pre>
<p>Complete方法，就是注册了一个Admission控制器，<a href="#WardleServerOptions-Complete">参考下文</a>。</p>
<p>Validate方法调用RecommendedOptions进行选项（合并了用户提供的命令行标记）合法性校验：</p>
<pre class="crayon-plain-tag">func (o WardleServerOptions) Validate(args []string) error {
	errors := []error{}
	// 校验结果是错误的切片
	errors = append(errors, o.RecommendedOptions.Validate()...)
	// 合并为单个错误
	return utilerrors.NewAggregate(errors)
}</pre>
<div class="blog_h3"><span class="graybg"><a id="start-server"></a>启动服务器</span></div>
<p>RunWardleServer方法启动API Server。它包含了将服务器选项（Option）转换为服务器配置（Config），从服务器配置实例化APIServer，并运行APIServer的整个流程：</p>
<pre class="crayon-plain-tag">func (o WardleServerOptions) RunWardleServer(stopCh &lt;-chan struct{}) error {
	// 选项转换为配置
	config, err := o.Config()
	if err != nil {
		return err
	}
	// 配置转换为CompletedConfig，实例化APIServer
	server, err := config.Complete().New()
	if err != nil {
		return err
	}

	// 注册一个在API Server启动之后运行的钩子
	server.GenericAPIServer.AddPostStartHookOrDie("start-sample-server-informers", func(context genericapiserver.PostStartHookContext) error {
		config.GenericConfig.SharedInformerFactory.Start(context.StopCh)
		o.SharedInformerFactory.Start(context.StopCh)
		return nil
	})

	//                             准备运行、  运行 APIServer
	return server.GenericAPIServer.PrepareRun().Run(stopCh)
}</pre>
<div class="blog_h3"><span class="graybg">服务器配置</span></div>
<p>API Server不能直接使用选项，必须将选项转换为apiserver.Config：</p>
<pre class="crayon-plain-tag">func (o *WardleServerOptions) Config() (*apiserver.Config, error) {
	// 这里又对选项进行了若干修改

	// 检查证书是否可以读取，如果不可以则尝试生成自签名证书
	if err := o.RecommendedOptions.SecureServing.MaybeDefaultWithSelfSignedCerts("localhost", nil, []net.IP{net.ParseIP("127.0.0.1")}); err != nil {
		return nil, fmt.Errorf("error creating self-signed certificates: %v", err)
	}
	// 根据特性开关，决定是否支持分页
	o.RecommendedOptions.Etcd.StorageConfig.Paging = utilfeature.DefaultFeatureGate.Enabled(features.APIListChunking)
	// 
	o.RecommendedOptions.ExtraAdmissionInitializers = func(c *genericapiserver.RecommendedConfig) ([]admission.PluginInitializer, error) {
		// ...
	}

	// 创建推荐配置
	serverConfig := genericapiserver.NewRecommendedConfig(apiserver.Codecs)
	// 暴露OpenAPI端点
	serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(
		// 自动生成的
		sampleopenapi.GetOpenAPIDefinitions, openapi.NewDefinitionNamer(apiserver.Scheme))
	serverConfig.OpenAPIConfig.Info.Title = "Wardle"
	serverConfig.OpenAPIConfig.Info.Version = "0.1"

	// 将RecommendedOptions应用到RecommendedConfig
	if err := o.RecommendedOptions.ApplyTo(serverConfig); err != nil {
		return nil, err
	}

	// 选项包含GenericConfig和你自定义的选项两部分
	config := &amp;apiserver.Config{
		GenericConfig: serverConfig,
		ExtraConfig:   apiserver.ExtraConfig{},
	}
	return config, nil
}</pre>
<p>从上面的代码可以看到RecommendedConfig是配置的核心：</p>
<pre class="crayon-plain-tag">type RecommendedConfig struct {
	// 用于配置GenericAPIServer的结构
	Config

	// SharedInformerFactory用于提供K8S资源的shared informers
	// 该字段由RecommendedOptions.CoreAPI.ApplyTo调用设置，informer默认使用in-cluster的ClientConfig
	SharedInformerFactory informers.SharedInformerFactory

	// 由RecommendedOptions.CoreAPI.ApplyTo设置，informer使用
	ClientConfig *restclient.Config
}

type Config struct {
	SecureServing *SecureServingInfo
	Authentication AuthenticationInfo
	Authorization AuthorizationInfo
	// 特权的本机使用的ClientConfig，PostStartHooks用到它
	LoopbackClientConfig *restclient.Config
	// ...
}</pre>
<p>通过调用ApplyTo方法，将RecommendedOptions中的选项传递给了RecommendedConfig：</p>
<pre class="crayon-plain-tag">func (o *RecommendedOptions) ApplyTo(config *server.RecommendedConfig) error {
	// 调用config.AddHealthChecks添加Etcd的健康检查
	// 设置config.RESTOptionsGetter
	if err := o.Etcd.ApplyTo(&amp;config.Config); err != nil {
		return err
	}
	// 创建config.Listener
	// 初始化config.Cert、config.CipherSuites、config.SNICerts等
	if err := o.SecureServing.ApplyTo(&amp;config.Config.SecureServing, &amp;config.Config.LoopbackClientConfig); err != nil {
		return err
	}
	// 初始化身份验证配置，获取相应的K8S客户端接口
	if err := o.Authentication.ApplyTo(&amp;config.Config.Authentication, config.SecureServing, config.OpenAPIConfig); err != nil {
		return err
	}
	// 初始化访问控制配置，获取相应的K8S客户端接口
	if err := o.Authorization.ApplyTo(&amp;config.Config.Authorization); err != nil {
		return err
	}
	if err := o.Audit.ApplyTo(&amp;config.Config, config.ClientConfig, config.SharedInformerFactory, o.ProcessInfo, o.Webhook); err != nil {
		return err
	}
	if err := o.Features.ApplyTo(&amp;config.Config); err != nil {
		return err
	}
	// 从配置文件加载kubeconfig或者使用incluster配置，提供config.ClientConfig和config.SharedInformerFactory
	if err := o.CoreAPI.ApplyTo(config); err != nil {
		return err
	}
	// 调用Admission初始化器
	if initializers, err := o.ExtraAdmissionInitializers(config); err != nil {
		return err
	// 逐个初始化Admission控制器
	} else if err := o.Admission.ApplyTo(&amp;config.Config, config.SharedInformerFactory, config.ClientConfig, o.FeatureGate, initializers...); err != nil {
		return err
	}
	if err := o.EgressSelector.ApplyTo(&amp;config.Config); err != nil {
		return err
	}
	if feature.DefaultFeatureGate.Enabled(features.APIPriorityAndFairness) {
		config.FlowControl = utilflowcontrol.New(
			config.SharedInformerFactory,
			kubernetes.NewForConfigOrDie(config.ClientConfig).FlowcontrolV1alpha1(),
			config.MaxRequestsInFlight+config.MaxMutatingRequestsInFlight,
			config.RequestTimeout/4,
		)
	}
	return nil
}</pre>
<p>可以看到，在生成服务器配置的阶段，对主kube-apiserver有强依赖，这些依赖导致了sample-apiserver无法脱离K8S独立运行。</p>
<p>RecommendedConfig还需要通过Conplete方法，变成CompletedConfig：</p>
<pre class="crayon-plain-tag">// server, err := config.Complete().New()

func (cfg *Config) Complete() CompletedConfig {
	c := completedConfig{
		cfg.GenericConfig.Complete(),
		&amp;cfg.ExtraConfig,
	}

	c.GenericConfig.Version = &amp;version.Info{
		Major: "1",
		Minor: "0",
	}

	return CompletedConfig{&amp;c}
}

// Complete补全缺失的、必须的配置信息，这些信息能够从已有配置导出
func (c *RecommendedConfig) Complete() CompletedConfig {
	return c.Config.Complete(c.SharedInformerFactory)
}</pre>
<div class="blog_h3"><span class="graybg">实例化APIServer</span></div>
<p>WardleServer，是从CompletedConfig实例化的：</p>
<pre class="crayon-plain-tag">func (c completedConfig) New() (*WardleServer, error) {
	// 创建GenericAPIServer
	//                                         名字用于在记录日志时进行区分
	//                                                           DelegationTarget用于进行APIServer的组合（composition）
	genericServer, err := c.GenericConfig.New("sample-apiserver", genericapiserver.NewEmptyDelegate())
	if err != nil {
		return nil, err
	}

	s := &amp;WardleServer{
		GenericAPIServer: genericServer,
	}</pre>
<p>上面的<pre class="crayon-plain-tag">New()</pre>创建了核心的GenericAPIServer：</p>
<pre class="crayon-plain-tag">func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*GenericAPIServer, error) {
	// 断言
	if c.Serializer == nil {
		return nil, fmt.Errorf("Genericapiserver.New() called with config.Serializer == nil")
	}
	if c.LoopbackClientConfig == nil {
		return nil, fmt.Errorf("Genericapiserver.New() called with config.LoopbackClientConfig == nil")
	}
	if c.EquivalentResourceRegistry == nil {
		return nil, fmt.Errorf("Genericapiserver.New() called with config.EquivalentResourceRegistry == nil")
	}

	handlerChainBuilder := func(handler http.Handler) http.Handler {
		return c.BuildHandlerChainFunc(handler, c.Config)
	}
	// 构建请求处理器
	apiServerHandler := NewAPIServerHandler(name, c.Serializer, handlerChainBuilder, delegationTarget.UnprotectedHandler())

	// 创建GenericAPIServer，很多字段直接来自completedConfig
	s := &amp;GenericAPIServer{
		discoveryAddresses:         c.DiscoveryAddresses,
		LoopbackClientConfig:       c.LoopbackClientConfig,
		legacyAPIGroupPrefixes:     c.LegacyAPIGroupPrefixes,
		admissionControl:           c.AdmissionControl,
		Serializer:                 c.Serializer,
		AuditBackend:               c.AuditBackend,
		Authorizer:                 c.Authorization.Authorizer,
		delegationTarget:           delegationTarget,
		EquivalentResourceRegistry: c.EquivalentResourceRegistry,
		HandlerChainWaitGroup:      c.HandlerChainWaitGroup,

		minRequestTimeout:     time.Duration(c.MinRequestTimeout) * time.Second,
		ShutdownTimeout:       c.RequestTimeout,
		ShutdownDelayDuration: c.ShutdownDelayDuration,
		SecureServingInfo:     c.SecureServing,
		ExternalAddress:       c.ExternalAddress,

		Handler: apiServerHandler,

		listedPathProvider: apiServerHandler,

		openAPIConfig:           c.OpenAPIConfig,
		skipOpenAPIInstallation: c.SkipOpenAPIInstallation,

		postStartHooks:         map[string]postStartHookEntry{},
		preShutdownHooks:       map[string]preShutdownHookEntry{},
		disabledPostStartHooks: c.DisabledPostStartHooks,

		healthzChecks:    c.HealthzChecks,
		livezChecks:      c.LivezChecks,
		readyzChecks:     c.ReadyzChecks,
		readinessStopCh:  make(chan struct{}),
		livezGracePeriod: c.LivezGracePeriod,

		DiscoveryGroupManager: discovery.NewRootAPIsHandler(c.DiscoveryAddresses, c.Serializer),

		maxRequestBodyBytes: c.MaxRequestBodyBytes,
		livezClock:          clock.RealClock{},
	}

	// ...

	// 添加delegationTarget的生命周期钩子
	for k, v := range delegationTarget.PostStartHooks() {
		s.postStartHooks[k] = v
	}
	for k, v := range delegationTarget.PreShutdownHooks() {
		s.preShutdownHooks[k] = v
	}

	// 添加预配置的钩子
	for name, preconfiguredPostStartHook := range c.PostStartHooks {
		if err := s.AddPostStartHook(name, preconfiguredPostStartHook.hook); err != nil {
			return nil, err
		}
	}

	// 如果配置包含了SharedInformerFactory，而启动该SharedInformerFactory的钩子没有注册
	// 则注册一个PostStart钩子来启动它
	genericApiServerHookName := "generic-apiserver-start-informers"
	if c.SharedInformerFactory != nil {
		if !s.isPostStartHookRegistered(genericApiServerHookName) {
			err := s.AddPostStartHook(genericApiServerHookName, func(context PostStartHookContext) error {
				c.SharedInformerFactory.Start(context.StopCh)
				return nil
			})
			if err != nil {
				return nil, err
			}
			// TODO: Once we get rid of /healthz consider changing this to post-start-hook.
			err = s.addReadyzChecks(healthz.NewInformerSyncHealthz(c.SharedInformerFactory))
			if err != nil {
				return nil, err
			}
		}
	}

	const priorityAndFairnessConfigConsumerHookName = "priority-and-fairness-config-consumer"
	if s.isPostStartHookRegistered(priorityAndFairnessConfigConsumerHookName) {
	} else if c.FlowControl != nil {
		err := s.AddPostStartHook(priorityAndFairnessConfigConsumerHookName, func(context PostStartHookContext) error {
			go c.FlowControl.Run(context.StopCh)
			return nil
		})
		if err != nil {
			return nil, err
		}
		// TODO(yue9944882): plumb pre-shutdown-hook for request-management system?
	} else {
		klog.V(3).Infof("Not requested to run hook %s", priorityAndFairnessConfigConsumerHookName)
	}

	// 添加delegationTarget的健康检查
	for _, delegateCheck := range delegationTarget.HealthzChecks() {
		skip := false
		for _, existingCheck := range c.HealthzChecks {
			if existingCheck.Name() == delegateCheck.Name() {
				skip = true
				break
			}
		}
		if skip {
			continue
		}
		s.AddHealthChecks(delegateCheck)
	}

	s.listedPathProvider = routes.ListedPathProviders{s.listedPathProvider, delegationTarget}

	// 安装Profiling、Metrics、URL / 下显示的路径列表（listedPathProvider）
	installAPI(s, c.Config)

	// use the UnprotectedHandler from the delegation target to ensure that we don't attempt to double authenticator, authorize,
	// or some other part of the filter chain in delegation cases.
	if delegationTarget.UnprotectedHandler() == nil &amp;&amp; c.EnableIndex {
		s.Handler.NonGoRestfulMux.NotFoundHandler(routes.IndexLister{
			StatusCode:   http.StatusNotFound,
			PathProvider: s.listedPathProvider,
		})
	}

	return s, nil
}</pre>
<p>GenericAPIServer.Handler就是HTTP请求的处理器，我们在下文的<a title="记录一次KeyDB缓慢的定位过程" href="#request-processing">请求处理过程</a>一节分析。</p>
<div class="blog_h3"><span class="graybg"><a id="InstallAPIGroup"></a>安装APIGroup</span></div>
<p>实例化GenericAPIServer之后，是安装APIGroup：</p>
<pre class="crayon-plain-tag">// 创建APIGroupInfo，关于一组API的各种信息，包括已经注册的API（Scheme），如何进行编解码（Codec）
	// 如何解析查询参数（ParameterCodec）
	apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(wardle.GroupName, Scheme, metav1.ParameterCodec, Codecs)

	// 从资源到rest.Storage的映射
	v1alpha1storage := map[stcongring]rest.Storage{}
	v1alpha1storage["flunders"] = wardleregistry.RESTInPeace(flunderstorage.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter))
	v1alpha1storage["fischers"] = wardleregistry.RESTInPeace(fischerstorage.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter))
	// 两个版本分别对应一个映射
	apiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = v1alpha1storage

	v1beta1storage := map[string]rest.Storage{}
	v1beta1storage["flunders"] = wardleregistry.RESTInPeace(flunderstorage.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter))
	apiGroupInfo.VersionedResourcesStorageMap["v1beta1"] = v1beta1storage
	if err := s.GenericAPIServer.InstallAPIGroup(&amp;apiGroupInfo); err != nil {
		return nil, err
	}

	return s, nil
}</pre>
<p>上面的flunderstorage.NewREST等方法返回的registry.REST，内嵌的genericregistry.Store不仅仅实现了rest.Storage：</p>
<pre class="crayon-plain-tag">type Storage interface {
	// 当请求的数据存放到该方法创建的对象之后，可以调用Create/Update进行持久化
	// 必须返回一个适用于 Codec.DecodeInto([]byte, runtime.Object) 的指针类型
	New() runtime.Object
}</pre>
<p>还实现了rest.StandardStorage：</p>
<pre class="crayon-plain-tag">type StandardStorage interface {
	Getter
	Lister
	CreaterUpdater
	GracefulDeleter
	CollectionDeleter
	Watcher
}</pre>
<p>实现了这些接口，意味着registry.REST能够支持API对象的增删改查和Watch。更多细节我们在下面的<a href="#request-processing">请求处理过程</a>一节中探讨。</p>
<div class="blog_h3"><span class="graybg">启动APIServer</span></div>
<p>如<a href="#start-server">启动服务器</a>一节中的代码所示， 在将选项转换为配置、完成配置，并从配置实例化APIServer之后，会执行由两个步骤组成的启动逻辑。</p>
<p>首先是PrepareRun，这里执行一些需要在API安装（在实例化时）之后进行的操作：</p>
<pre class="crayon-plain-tag">func (s *GenericAPIServer) PrepareRun() preparedGenericAPIServer {
	s.delegationTarget.PrepareRun()
	// 安装OpenAPI的Handler
	if s.openAPIConfig != nil &amp;&amp; !s.skipOpenAPIInstallation {
		s.OpenAPIVersionedService, s.StaticOpenAPISpec = routes.OpenAPI{
			Config: s.openAPIConfig,
		}.Install(s.Handler.GoRestfulContainer, s.Handler.NonGoRestfulMux)
	}
	// 安装健康检查的Handler
	s.installHealthz()
	s.installLivez()
	err := s.addReadyzShutdownCheck(s.readinessStopCh)
	if err != nil {
		klog.Errorf("Failed to install readyz shutdown check %s", err)
	}
	s.installReadyz()

	// 为审计后端注册关闭前钩子
	if s.AuditBackend != nil {
		err := s.AddPreShutdownHook("audit-backend", func() error {
			s.AuditBackend.Shutdown()
			return nil
		})
		if err != nil {
			klog.Errorf("Failed to add pre-shutdown hook for audit-backend %s", err)
		}
	}

	return preparedGenericAPIServer{s}
}</pre>
<p>然后是Run，启动APIServer：</p>
<pre class="crayon-plain-tag">func (s preparedGenericAPIServer) Run(stopCh &lt;-chan struct{}) error {
	delayedStopCh := make(chan struct{})
	go func() {
		defer close(delayedStopCh)
		// 收到关闭信号
		&lt;-stopCh
		// 一旦关闭流程被触发，/readyz就需要立刻返回错误
		close(s.readinessStopCh)
		// 关闭服务器前休眠ShutdownDelayDuration，这让LB有个时间窗口来检测/readyz状态，不再发送请求给此服务器
		time.Sleep(s.ShutdownDelayDuration)
	}()

	// 运行服务器
	err := s.NonBlockingRun(delayedStopCh)
	if err != nil {
		return err
	}

	// 收到关闭信号
	&lt;-stopCh

	// 运行关闭前钩子
	err = s.RunPreShutdownHooks()
	if err != nil {
		return err
	}

	// 等待延迟关闭信号
	&lt;-delayedStopCh

	// 等待现有请求完毕，然后关闭
	s.HandlerChainWaitGroup.Wait()

	return nil
}</pre>
<p> NonBlockingRun是启动APIServer的核心代码。它会启动一个HTTPS服务器：</p>
<pre class="crayon-plain-tag">func (s preparedGenericAPIServer) NonBlockingRun(stopCh &lt;-chan struct{}) error {
	// 这个通道用于保证HTTP Server优雅关闭，不会导致丢失审计事件
	auditStopCh := make(chan struct{})

	// 首先启动审计后端，这时任何请求都进不来
	if s.AuditBackend != nil {
		if err := s.AuditBackend.Run(auditStopCh); err != nil {
			return fmt.Errorf("failed to run the audit backend: %v", err)
		}
	}

	// 下面的通道用于出错时清理listener
	internalStopCh := make(chan struct{})
	var stoppedCh &lt;-chan struct{}
	if s.SecureServingInfo != nil &amp;&amp; s.Handler != nil {
		var err error
		// 启动HTTPS服务器，仅当证书错误或内部listen调用出错时会失败
		// server loop在一个Goroutine中运行
		// 可以看到，这里的s.Handler是可以被我们访问到的，因此建立HTTP（非HTTPS）服务器应该很方便
		stoppedCh, err = s.SecureServingInfo.Serve(s.Handler, s.ShutdownTimeout, internalStopCh)
		if err != nil {
			close(internalStopCh)
			close(auditStopCh)
			return err
		}
	}

	// 清理
	go func() {
		&lt;-stopCh
		close(internalStopCh)
		if stoppedCh != nil {
			&lt;-stoppedCh
		}
		s.HandlerChainWaitGroup.Wait()
		close(auditStopCh)
	}()

	// 启动后钩子
	s.RunPostStartHooks(stopCh)

	if _, err := systemd.SdNotify(true, "READY=1\n"); err != nil {
		klog.Errorf("Unable to send systemd daemon successful start message: %v\n", err)
	}

	return nil
}</pre>
<div class="blog_h3"><span class="graybg">结构注册到Scheme</span></div>
<p>apis/wardle包，以及它的子包，定义了wardle.example.com组的API。</p>
<p>wardle包的register.go中定义了组，以及从GV获得GVK、GVR的函数：</p>
<pre class="crayon-plain-tag">const GroupName = "wardle.example.com"
//                                                             没有正式（formal）类型则使用此常量
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}

func Kind(kind string) schema.GroupKind {
	return SchemeGroupVersion.WithKind(kind).GroupKind()
}

func Resource(resource string) schema.GroupResource {
	return SchemeGroupVersion.WithResource(resource).GroupResource()
}</pre>
<p>API包通常都要提供AddToScheme变量，用于将API注册到指定的scheme：</p>
<pre class="crayon-plain-tag">var (
	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
	AddToScheme   = SchemeBuilder.AddToScheme
)

func addKnownTypes(scheme *runtime.Scheme) error {
	// 注册API的核心，添加GV中的若干API类型
	scheme.AddKnownTypes(SchemeGroupVersion,
		&amp;Flunder{}, // 必须提供指针类型，Go反射得到类型在编码时作为Kind
		&amp;FlunderList{},
		&amp;Fischer{},
		&amp;FischerList{},
	)
	return nil
}

// 所谓SchemeBuilder其实就是切片
type SchemeBuilder []func(*Scheme) error
// NewSchemeBuilder方法支持多个回调函数，对这些函数调用SchemeBuilder.Register
func NewSchemeBuilder(funcs ...func(*Scheme) error) SchemeBuilder {
	var sb SchemeBuilder
	sb.Register(funcs...)
	return sb
}
// 所谓Register就是添加到切片中
func (sb *SchemeBuilder) Register(funcs ...func(*Scheme) error) {
	for _, f := range funcs {
		*sb = append(*sb, f)
	}
}

// 所谓AddToScheme就是遍历回调，Go方法可以作为变量传递
func (sb *SchemeBuilder) AddToScheme(s *Scheme) error {
	for _, f := range *sb {
		if err := f(s); err != nil {
			return err
		}
	}
	return nil
}</pre>
<p>wardle包的types.go则定义了API类型对应的Go结构体。</p>
<p>子包v1alpha1、v1beta1定义了API的两个版本。它们包含和wardle包类似的GroupName、SchemeGroupVersion、SchemeBuilder、AddToScheme…等变量/函数，以及对应的API类型结构体。还包括自动生成的、与APIVersionInternal版本API进行转换的函数。</p>
<p>回到wardle包，Install方法支持将APIVersionInternal、v1alpha1、v1beta1这些版本都注册到scheme：</p>
<pre class="crayon-plain-tag">func Install(scheme *runtime.Scheme) {
	utilruntime.Must(wardle.AddToScheme(scheme))
	utilruntime.Must(v1beta1.AddToScheme(scheme))
	utilruntime.Must(v1alpha1.AddToScheme(scheme))
	utilruntime.Must(scheme.SetVersionPriority(v1beta1.SchemeGroupVersion, v1alpha1.SchemeGroupVersion))
}</pre>
<p>而在apiserver包中，在init时会调用上面的Install函数：</p>
<pre class="crayon-plain-tag">var (
	// API注册表
	Scheme = runtime.NewScheme()
	// 编解码器工厂
	Codecs = serializer.NewCodecFactory(Scheme)
)

func init() {
	// 安装sample-apiserver提供的API
	install.Install(Scheme)

	// 将k8s.io/apimachinery/pkg/apis/meta/v1"注册到v1组，为什么？
	// we need to add the options to empty v1
	// TODO fix the server code to avoid this
	metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})

	// TODO: keep the generic API server from wanting this
	unversioned := schema.GroupVersion{Group: "", Version: "v1"}
	Scheme.AddUnversionedTypes(unversioned,
		&amp;metav1.Status{},
		&amp;metav1.APIVersions{},
		&amp;metav1.APIGroupList{},
		&amp;metav1.APIGroup{},
		&amp;metav1.APIResourceList{},
	)
}</pre>
<p>Codecs是API资源编解码器的工厂，RecommendedOptions需要此工厂：</p>
<pre class="crayon-plain-tag">// ... 
		RecommendedOptions: genericoptions.NewRecommendedOptions(
			defaultEtcdPathPrefix,
			// 得到v1alpha1版本的LegacyCodec，遗留编解码器（runtime.Codec）
			// LegacyCodec编码到指定的API版本，解码（从任何支持的源）到内部形式
			// 此编码器总是编码为JSON
			// 
			// LegacyCodec方法已经废弃，客户端/服务器应该根据MIME类型协商serializer
			// 并调用CodecForVersions
			apiserver.Codecs.LegacyCodec(v1alpha1.SchemeGroupVersion),
			genericoptions.NewProcessInfo("wardle-apiserver", "wardle"),
		),

func NewRecommendedOptions(prefix string, codec runtime.Codec, processInfo *ProcessInfo) *RecommendedOptions {
	return &amp;RecommendedOptions{
		//  不过这里的runtime.Codec用于将AP对象转换为JSON并存放到Etcd，而不是发给客户端
		Etcd:           NewEtcdOptions(storagebackend.NewDefaultConfig(prefix, codec)),
	// ...</pre>
<p>APIGroupInfo也需要此工厂：</p>
<pre class="crayon-plain-tag">apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(wardle.GroupName, Scheme, metav1.ParameterCodec, Codecs)

func NewDefaultAPIGroupInfo(group string, scheme *runtime.Scheme, parameterCodec runtime.ParameterCodec, codecs serializer.CodecFactory) APIGroupInfo {
	return APIGroupInfo{
		// ...
		Scheme:                 scheme,
		ParameterCodec:         parameterCodec,
		NegotiatedSerializer:   codecs,
	}
}</pre>
<p>在APIServer实例化期间，Scheme会传递给APIGroupInfo，从而实现资源 - Go类型之间的映射。</p>
<div class="blog_h3"><span class="graybg">Admission控制器</span></div>
<p>sample-server也示例了如何集成自己的Admission控制器到API Server中。</p>
<p>在选项中，需要注册一个Admission初始化器</p>
<pre class="crayon-plain-tag">o.RecommendedOptions.ExtraAdmissionInitializers = func(c *genericapiserver.RecommendedConfig) ([]admission.PluginInitializer, error) {
		client, err := clientset.NewForConfig(c.LoopbackClientConfig)
		if err != nil {
			return nil, err
		}
		informerFactory := informers.NewSharedInformerFactory(client, c.LoopbackClientConfig.Timeout)
		o.SharedInformerFactory = informerFactory
		//                                   初始化函数
		return []admission.PluginInitializer{wardleinitializer.New(informerFactory)}, nil
	}</pre>
<p>初始化器是一个函数，会在选项转为配置的时候执行：</p>
<pre class="crayon-plain-tag">// 调用上面的函数
	if initializers, err := o.ExtraAdmissionInitializers(config); err != nil {
		return err
	// 初始化Admission控制器
	} else if err := o.Admission.ApplyTo(&amp;config.Config, config.SharedInformerFactory, config.ClientConfig, o.FeatureGate, initializers...); err != nil {
		return err
	}</pre>
<p>上面的ApplyTo方法，会组建一个Admission控制器的初始化函数链，并逐个调用，以初始化所有Admission控制器。</p>
<p>此外，在PostStart钩子中，会启动Admission控制器所依赖的SharedInformerFactory：</p>
<pre class="crayon-plain-tag">server.GenericAPIServer.AddPostStartHookOrDie("start-sample-server-informers", func(context genericapiserver.PostStartHookContext) error {
		// 主kube-apiserver的InformerFactory，貌似没什么用
		config.GenericConfig.SharedInformerFactory.Start(context.StopCh)
		// 次级kube-apiserver的InformerFactory，Admission控制器需要使用
		o.SharedInformerFactory.Start(context.StopCh)
		return nil
	})</pre>
<p>我们看一下sample-apiserver的Admission相关代码。</p>
<p>位于pkg/admission/wardleinitializer包中的是Admission初始化器，它能够为任何WantsInternalWardleInformerFactory类型的Admission控制器注入InformerFactory：</p>
<pre class="crayon-plain-tag">type pluginInitializer struct {
	informers informers.SharedInformerFactory
}

var _ admission.PluginInitializer = pluginInitializer{}

// 该函数在ExtraAdmissionInitializers函数中调用
func New(informers informers.SharedInformerFactory) pluginInitializer {
	return pluginInitializer{
		informers: informers,
	}
}

// 该函数在o.Admission.ApplyTo中调用
func (i pluginInitializer) Initialize(plugin admission.Interface) {
	if wants, ok := plugin.(WantsInternalWardleInformerFactory); ok {
		wants.SetInternalWardleInformerFactory(i.informers)
	}
}</pre>
<p>位于pkg/admission/plugin/banflunder包中的是为了的Admission控制器BanFlunder。函数：</p>
<pre class="crayon-plain-tag">func Register(plugins *admission.Plugins) {
	plugins.Register("BanFlunder", func(config io.Reader) (admission.Interface, error) {
		return New()
	})
}</pre>
<p><a id="WardleServerOptions-Complete"></a>会在程序运行的很早期调用，以注册Admission控制器到API Server：</p>
<pre class="crayon-plain-tag">func (o *WardleServerOptions) Complete() error {
	// 注册插件
	banflunder.Register(o.RecommendedOptions.Admission.Plugins)

	// 配置顺序
	o.RecommendedOptions.Admission.RecommendedPluginOrder = append(o.RecommendedOptions.Admission.RecommendedPluginOrder, "BanFlunder")

	return nil
}</pre>
<p>Admission控制器的核心是Admit函数，它可以修改或否决一个API Server请求：</p>
<pre class="crayon-plain-tag">func (d *DisallowFlunder) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
	// 仅仅对特定类型的API感兴趣
	if a.GetKind().GroupKind() != wardle.Kind("Flunder") {
		return nil
	}

	if !d.WaitForReady() {
		return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
	}

	// 用于获取元数据
	metaAccessor, err := meta.Accessor(a.GetObject())
	if err != nil {
		return err
	}
	flunderName := metaAccessor.GetName()

	fischers, err := d.lister.List(labels.Everything())
	if err != nil {
		return err
	}

	for _, fischer := range fischers {
		for _, disallowedFlunder := range fischer.DisallowedFlunders {
			if flunderName == disallowedFlunder {
				return errors.NewForbidden(
					a.GetResource().GroupResource(),
					a.GetName(),
					// 拒绝请求
					fmt.Errorf("this name may not be used, please change the resource name"),
				)
			}
		}
	}
	return nil
}</pre>
<div class="blog_h3"><span class="graybg"><a id="request-processing"></a>请求处理过程</span></div>
<p>通过上文的分析，我们知道资源的增删改查是由registry.REST（空壳，实际是genericregistry.Store）负责的，那么HTTP请求是如何传递给它的呢？</p>
<p>回顾一下向APIGroupInfo中添加资源的代码：</p>
<pre class="crayon-plain-tag">//              资源类型          
v1alpha1storage["flunders"] = wardleregistry.RESTInPeace(
	// 创建registry.REST
	flunderstorage.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter)
)


func NewREST(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*registry.REST, error) {
	// 策略，参考下文
	strategy := NewStrategy(scheme)

	store := &amp;genericregistry.Store{
		// 实例化资源的函数
		NewFunc:                  func() runtime.Object {
			// 每次增删改查，都牵涉到结构的创建，因此在此打断点可以拦截所有请求
			return &amp;wardle.Flunder{}
		},
		// 实例化资源列表的函数
		NewListFunc:              func() runtime.Object { return &amp;wardle.FlunderList{} },
		// 判断对象是否可以被该存储处理
		PredicateFunc: func(label labels.Selector, field fields.Selector) storage.SelectionPredicate {
			return storage.SelectionPredicate{
				Label: label,
				Field: field,
				GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, error) {
					apiserver, ok := obj.(*wardle.Flunder)
					if !ok {
						return nil, nil, fmt.Errorf("given object is not a Flunder")
					}
					return apiserver.ObjectMeta.Labels, SelectableFields(apiserver), nil
				},
			}
		},
		// 资源的复数名称，当上下文中缺少必要的请求信息时使用
		DefaultQualifiedResource: wardle.Resource("flunders"),
		// 增删改的策略
		CreateStrategy: strategy,
		UpdateStrategy: strategy,
		DeleteStrategy: strategy,
	}
	options := &amp;generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}
	// 填充默认字段
	if err := store.CompleteWithOptions(options); err != nil {
		return nil, err
	}
	return &amp;registry.REST{store}, nil
}</pre>
<p>为了创建genericregistry.Store，需要两个信息：</p>
<ol>
<li>Scheme，它提供的信息是Go类型和GVK之间的映射关系。其中Kind是根据Go结构的类型名反射得到的</li>
<li>generic.RESTOptionsGetter</li>
</ol>
<p>RESTOptionsGetter用于获得RESTOptions：</p>
<pre class="crayon-plain-tag">type RESTOptionsGetter interface {
	GetRESTOptions(resource schema.GroupResource) (RESTOptions, error)
}</pre>
<p>RESTOptions包含了关于存储的信息，尽管这个职责和名字好像没什么关系：</p>
<pre class="crayon-plain-tag">type RESTOptions struct {
	// 创建一个存储后端所需的配置信息
	StorageConfig *storagebackend.Config
	// 这是一个函数，能够提供storage.Interface和factory.DestroyFunc
	Decorator     StorageDecorator

	EnableGarbageCollection bool
	DeleteCollectionWorkers int
	ResourcePrefix          string
	CountMetricPollPeriod   time.Duration
}

// 
type Config struct {
	// 存储后端的类型，默认etcd3
	Type string
	// 传递给storage.Interface的所有方法的前缀，对应etcd存储前缀
	Prefix string
	// 连接到Etcd服务器的相关信息
	Transport TransportConfig
	// 提示APIServer是否应该支持分页
	Paging bool
	// 负责（反）串行化
	Codec runtime.Codec
	// 在持久化到Etcd之前，该对象输出目标将被转换为的GVK
	Transformer value.Transformer

	CompactionInterval time.Duration
	CountMetricPollPeriod time.Duration
	LeaseManagerConfig etcd3.LeaseManagerConfig
}


// 利用入参函数，构造storage.Interface
type StorageDecorator func(
	config *storagebackend.Config,
	resourcePrefix string,
	keyFunc func(obj runtime.Object) (string, error),
	newFunc func() runtime.Object,
	newListFunc func() runtime.Object,
	getAttrsFunc storage.AttrFunc,
	trigger storage.IndexerFuncs,
	indexers *cache.Indexers) (storage.Interface, factory.DestroyFunc, error)
// CRUD
type Interface interface {
	Versioner() Versioner
	Create(ctx context.Context, key string, obj, out runtime.Object, ttl uint64) error
	Delete(ctx context.Context, key string, out runtime.Object, preconditions *Preconditions, validateDeletion ValidateObjectFunc) error
	Watch(ctx context.Context, key string, resourceVersion string, p SelectionPredicate) (watch.Interface, error)
	WatchList(ctx context.Context, key string, resourceVersion string, p SelectionPredicate) (watch.Interface, error)
	Get(ctx context.Context, key string, resourceVersion string, objPtr runtime.Object, ignoreNotFound bool) error
	GetToList(ctx context.Context, key string, resourceVersion string, p SelectionPredicate, listObj runtime.Object) error
	List(ctx context.Context, key string, resourceVersion string, p SelectionPredicate, listObj runtime.Object) error
	GuaranteedUpdate(
		ctx context.Context, key string, ptrToType runtime.Object, ignoreNotFound bool,
		precondtions *Preconditions, tryUpdate UpdateFunc, suggestion ...runtime.Object) error
	Count(key string) (int64, error)
}
// 这个函数用于一次性销毁任何Create()创建的、当前Storage使用的对象
type DestroyFunc func()</pre>
<p>在这里我们可以注意到storagebackend.Config和Etcd是有耦合的，因此想支持其它存储后端，需要在更早的节点介入。</p>
<p>genericregistry.Store还包含了三个Strategy字段：</p>
<pre class="crayon-plain-tag">type Store struct {
	// ...
	CreateStrategy rest.RESTCreateStrategy
	UpdateStrategy rest.RESTUpdateStrategy
	DeleteStrategy rest.RESTDeleteStrategy
	// ...
}

type RESTCreateStrategy interface {
	runtime.ObjectTyper
	// 用于生成名称
	names.NameGenerator
	// 对象是否必须在命名空间中
	NamespaceScoped() bool
	// 在Validate、Canonicalize之前调用，进行对象的normalize
	// 例如删除不需要持久化的字段、对顺序不敏感的列表字段进行重新排序
	// 不得移除这样的字段：它不能通过校验
	PrepareForCreate(ctx context.Context, obj runtime.Object)
	// 校验，在对象的默认字段被填充后调用
	Validate(ctx context.Context, obj runtime.Object) field.ErrorList
	// 在校验之后，持久化之前调用。正规化对象，通常实现为类型检查，或空函数
	Canonicalize(obj runtime.Object)
}

type RESTUpdateStrategy interface {
	runtime.ObjectTyper
	NamespaceScoped() bool
	// 对象是否可以被PUT请求创建
	AllowCreateOnUpdate() bool
	// 准备更新
	PrepareForUpdate(ctx context.Context, obj, old runtime.Object)
	// 校验
	ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList
	// 正规化
	Canonicalize(obj runtime.Object)
	// 当对象上没有指定版本时，是否允许无条件的更新，也就是不管最新的资源版本（禁用乐观并发控制）
	AllowUnconditionalUpdate() bool
}

type RESTDeleteStrategy interface {
	runtime.ObjectTyper
}

type ObjectTyper interface {
	// 得到对象可能的GVK信息
	ObjectKinds(Object) ([]schema.GroupVersionKind, bool, error)
	// Scheme是否支持指定的GVK
	Recognizes(gvk schema.GroupVersionKind) bool
}</pre>
<p>可以看到，策略能够影响增删改的行为，它能够生成对象名称，校验对象合法性，甚至修改对象。 我们看一下sample-apiserver提供的策略实现：</p>
<pre class="crayon-plain-tag">func NewStrategy(typer runtime.ObjectTyper) flunderStrategy {
	// 简单命名策略：返回请求的basename外加5位字母数字的随即后缀
	return flunderStrategy{typer, names.SimpleNameGenerator}
}

type flunderStrategy struct {
	runtime.ObjectTyper
	names.NameGenerator
}

func (flunderStrategy) NamespaceScoped() bool {
	return true
}

func (flunderStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
}

func (flunderStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
}

func (flunderStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
	flunder := obj.(*wardle.Flunder)
	return validation.ValidateFlunder(flunder)
}

func (flunderStrategy) AllowCreateOnUpdate() bool {
	return false
}

func (flunderStrategy) AllowUnconditionalUpdate() bool {
	return false
}

func (flunderStrategy) Canonicalize(obj runtime.Object) {
}

func (flunderStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
	return field.ErrorList{}
}



package validation

// ValidateFlunder validates a Flunder.
func ValidateFlunder(f *wardle.Flunder) field.ErrorList {
	allErrs := field.ErrorList{}

	allErrs = append(allErrs, ValidateFlunderSpec(&amp;f.Spec, field.NewPath("spec"))...)

	return allErrs
}

// ValidateFlunderSpec validates a FlunderSpec.
func ValidateFlunderSpec(s *wardle.FlunderSpec, fldPath *field.Path) field.ErrorList {
	allErrs := field.ErrorList{}

	if len(s.FlunderReference) != 0 &amp;&amp; len(s.FischerReference) != 0 {
		allErrs = append(allErrs, field.Invalid(fldPath.Child("fischerReference"), s.FischerReference, "cannot be set with flunderReference at the same time"))
	} else if len(s.FlunderReference) != 0 &amp;&amp; s.ReferenceType != wardle.FlunderReferenceType {
		allErrs = append(allErrs, field.Invalid(fldPath.Child("flunderReference"), s.FlunderReference, "cannot be set if referenceType is not Flunder"))
	} else if len(s.FischerReference) != 0 &amp;&amp; s.ReferenceType != wardle.FischerReferenceType {
		allErrs = append(allErrs, field.Invalid(fldPath.Child("fischerReference"), s.FischerReference, "cannot be set if referenceType is not Fischer"))
	} else if len(s.FischerReference) == 0 &amp;&amp; s.ReferenceType == wardle.FischerReferenceType {
		allErrs = append(allErrs, field.Invalid(fldPath.Child("fischerReference"), s.FischerReference, "cannot be empty if referenceType is Fischer"))
	} else if len(s.FlunderReference) == 0 &amp;&amp; s.ReferenceType == wardle.FlunderReferenceType {
		allErrs = append(allErrs, field.Invalid(fldPath.Child("flunderReference"), s.FlunderReference, "cannot be empty if referenceType is Flunder"))
	}

	if len(s.ReferenceType) != 0 &amp;&amp; s.ReferenceType != wardle.FischerReferenceType &amp;&amp; s.ReferenceType != wardle.FlunderReferenceType {
		allErrs = append(allErrs, field.Invalid(fldPath.Child("referenceType"), s.ReferenceType, "must be Flunder or Fischer"))
	}

	return allErrs
}</pre>
<p>分析到这里，我们可以猜测到Create请求被genericregistry.Store处理的过程：</p>
<ol>
<li>读取请求体，调用NewFunc反串行化为runtime.Obejct</li>
<li>调用PredicateFunc判断是否能够处理该对象</li>
<li>调用CreateStrategy，校验、正规化对象</li>
<li>调用RESTOptions，存储到Etcd</li>
</ol>
<p>那么，请求是如何传递过来的，上述处理的细节又是怎样的？上文中我们已经定位到关键代码路径，通过断点很容易跟踪到完整处理流程。</p>
<p>在上文分析的GenericAPIServer实例过程中，它的Handler字段是这样创建的：</p>
<pre class="crayon-plain-tag">apiServerHandler := NewAPIServerHandler(name, c.Serializer, handlerChainBuilder, delegationTarget.UnprotectedHandler())

// 处理器链的构建器，注意出入参类型一样。这是因为处理器链是一层层包裹的，而不是链表那样的结构
type HandlerChainBuilderFn func(apiHandler http.Handler) http.Handler

func NewAPIServerHandler(name string, s runtime.NegotiatedSerializer, 
		handlerChainBuilder HandlerChainBuilderFn, notFoundHandler http.Handler) *APIServerHandler {

	// 配置go-restful，go-restful是一个构建REST风格WebService的框架
	nonGoRestfulMux := mux.NewPathRecorderMux(name)
	if notFoundHandler != nil {
		nonGoRestfulMux.NotFoundHandler(notFoundHandler)
	}

	// 容器，是一组WebService的集合
	gorestfulContainer := restful.NewContainer()
	// 容器包含一个用户HTTP请求多路复用的ServeMux
	gorestfulContainer.ServeMux = http.NewServeMux()
	// 路由器
	gorestfulContainer.Router(restful.CurlyRouter{}) // e.g. for proxy/{kind}/{name}/{*}
	// panic处理器
	gorestfulContainer.RecoverHandler(func(panicReason interface{}, httpWriter http.ResponseWriter) {
		logStackOnRecover(s, panicReason, httpWriter)
	})
	// 错误处理器
	gorestfulContainer.ServiceErrorHandler(func(serviceErr restful.ServiceError, request *restful.Request, response *restful.Response) {
		serviceErrorHandler(s, serviceErr, request, response)
	})

	director := director{
		name:               name,
		goRestfulContainer: gorestfulContainer,
		nonGoRestfulMux:    nonGoRestfulMux,
	}

	return &amp;APIServerHandler{
		// 构建处理器链，                 注意传入的director
		FullHandlerChain:   handlerChainBuilder(director),
		GoRestfulContainer: gorestfulContainer,
		NonGoRestfulMux:    nonGoRestfulMux,
		Director:           director,
	}
}


type APIServerHandler struct {
	// 处理器链，接口是http包中标准的：
	// type Handler interface {
	// 	ServeHTTP(ResponseWriter, *Request)
	// }
	// 它组织一系列的过滤器，并在请求通过过滤器链后，调用Director
	FullHandlerChain http.Handler
	// 所有注册的API由此容器处理
	// InstallAPIs使用该字段，其他server不应该直接访问
	GoRestfulContainer *restful.Container
	// 链中最后一个处理器。这个类型包装一个mux对象，并且记录下注册了哪些URL路径
	NonGoRestfulMux *mux.PathRecorderMux

	// Director用于处理fall through和proxy
	Director http.Handler
}</pre>
<p> 这个Handler实现了http.Handler，简单的把请求委托给FullHandlerChain处理：</p>
<pre class="crayon-plain-tag">func (a *APIServerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	a.FullHandlerChain.ServeHTTP(w, r)
}</pre>
<p>上面这个函数，就是所有HTTP请求处理的入口点。</p>
<p>FullHandlerChain是handlerChainBuilder()调用得到的：</p>
<pre class="crayon-plain-tag">var c completedConfig
handlerChainBuilder := func(handler http.Handler) http.Handler {
	return c.BuildHandlerChainFunc(handler, c.Config)
}
apiServerHandler := NewAPIServerHandler(name, c.Serializer, handlerChainBuilder, delegationTarget.UnprotectedHandler())</pre>
<p>completedConfig.BuildHandlerChainFunc则来自于它内嵌的Config：</p>
<pre class="crayon-plain-tag">serverConfig := genericapiserver.NewRecommendedConfig(apiserver.Codecs)
func NewRecommendedConfig(codecs serializer.CodecFactory) *RecommendedConfig {
	return &amp;RecommendedConfig{
		Config: *NewConfig(codecs),
	}
}
func NewConfig(codecs serializer.CodecFactory) *Config {
	defaultHealthChecks := []healthz.HealthChecker{healthz.PingHealthz, healthz.LogHealthz}
	return &amp;Config{
		Serializer:                  codecs,
		BuildHandlerChainFunc:       DefaultBuildHandlerChain,
	// ...
}

func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
	// 最内层：传入的apiHandler，也就是上文中的director
	// 访问控制
	handler := genericapifilters.WithAuthorization(apiHandler, c.Authorization.Authorizer, c.Serializer)
	// 访问速率控制
	if c.FlowControl != nil {
		handler = genericfilters.WithPriorityAndFairness(handler, c.LongRunningFunc, c.FlowControl)
	} else {
		handler = genericfilters.WithMaxInFlightLimit(handler, c.MaxRequestsInFlight, c.MaxMutatingRequestsInFlight, c.LongRunningFunc)
	}
	// 身份扮演
	handler = genericapifilters.WithImpersonation(handler, c.Authorization.Authorizer, c.Serializer)
	// 审计
	handler = genericapifilters.WithAudit(handler, c.AuditBackend, c.AuditPolicyChecker, c.LongRunningFunc)
	// 处理身份认证失败
	failedHandler := genericapifilters.Unauthorized(c.Serializer, c.Authentication.SupportsBasicAuth)
	failedHandler = genericapifilters.WithFailedAuthenticationAudit(failedHandler, c.AuditBackend, c.AuditPolicyChecker)
	// 身份验证
	handler = genericapifilters.WithAuthentication(handler, c.Authentication.Authenticator, failedHandler, c.Authentication.APIAudiences)
	// 处理CORS请求
	handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true")
	// 超时处理
	handler = genericfilters.WithTimeoutForNonLongRunningRequests(handler, c.LongRunningFunc, c.RequestTimeout)
	// 所有长时间运行请求被添加到等待组，用于优雅关机
	handler = genericfilters.WithWaitGroup(handler, c.LongRunningFunc, c.HandlerChainWaitGroup)
	// 将RequestInfo附加到Context对象
	handler = genericapifilters.WithRequestInfo(handler, c.RequestInfoResolver)
	// HTTP Goway处理
	if c.SecureServing != nil &amp;&amp; !c.SecureServing.DisableHTTP2 &amp;&amp; c.GoawayChance &gt; 0 {
		handler = genericfilters.WithProbabilisticGoaway(handler, c.GoawayChance)
	}
	// 设置Cache-Control头为"no-cache, private"，因为所有server被authn/authz保护
	handler = genericapifilters.WithCacheControl(handler)
	// 崩溃恢复
	handler = genericfilters.WithPanicRecovery(handler)
	return handler
}</pre>
<p>我们可以清楚的看到默认的处理器链包含的大量过滤器，以及处理器是一层层包裹而非链表结构。</p>
<p>最后，我们来从头跟踪一下请求处理过程，首先看看处理器链中的过滤器们：</p>
<pre class="crayon-plain-tag">func (a *APIServerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	a.FullHandlerChain.ServeHTTP(w, r)
}
// FullHandlerChain是这个类型
type HandlerFunc func(ResponseWriter, *Request)
// 巧妙的设计，实现了
// type Handler interface {
//	ServeHTTP(ResponseWriter, *Request)
// }
// 接口，但是这个实现，直接委托给类型对应的函数
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

// 进入处理器链的最外层
func withPanicRecovery(handler http.Handler, crashHandler func(http.ResponseWriter, *http.Request, interface{})) http.Handler {
	handler = httplog.WithLogging(handler, httplog.DefaultStacktracePred)
	//                      处理函数
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		defer runtime.HandleCrash(func(err interface{}) {
			crashHandler(w, req, err)
		})

		// 转发给内层处理器，内层处理器通过闭包捕获
		handler.ServeHTTP(w, req)
	})
}

// 省略若干中间的处理器...

// 这个处理器值得注意，它将请求信息存放到context中，内层处理器可以直接使用这些信息
// 信息包括：
type RequestInfo struct {
	// 是否针对资源/子资源的请求
	IsResourceRequest bool
	// URL的路径部分
	Path string
	// 小写的HTTP动词
	Verb string
	// API前缀
	APIPrefix  string
	// API组
	APIGroup   string
	// API版本
	APIVersion string
	// 命名空间
	Namespace  string
	// 资源类型名，通常是小写的复数形式，而不是Kind
	Resource string
	// 请求的子资源，子资源是scoped to父资源的另外一个资源，可以具有不同的Kind
	// 例如 /pods对应资源"pods"，对应Kind为"Pod"
	//      /pods/foo/status对应资源"pods"，对应子资源 "status"，对应Kind为"Pod"
	// 然而 /pods/foo/binding对应资源"pods"，对应子资源 "binding", 而对应Kind为"Binding"
	Subresource string
	// 对于某些资源，名字是空的。如果请求指示一个名字（不在请求体中）则填写在该字段
	// Parts are the path parts for the request, always starting with /{resource}/{name}
	Parts []string
}
func WithRequestInfo(handler http.Handler, resolver request.RequestInfoResolver) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		ctx := req.Context()
		info, err := resolver.NewRequestInfo(req)
		if err != nil {
			responsewriters.InternalError(w, req, fmt.Errorf("failed to create RequestInfo: %v", err))
			return
		}

		req = req.WithContext(request.WithRequestInfo(ctx, info))

		handler.ServeHTTP(w, req)
	})
}

// 省略若干中间的处理器...</pre>
<p>遍历所有过滤器后，来到处理器链的最后一环，也就是director。从名字上可以看到，它在整体上负责请求的分发：</p>
<pre class="crayon-plain-tag">func (d director) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	path := req.URL.Path

	// 遍历已经注册的所有WebService，看看有没有负责当前URL路径的
	for _, ws := range d.goRestfulContainer.RegisteredWebServices() {
		switch {
		case ws.RootPath() == "/apis":
			// 如果URL路径是 /apis或/apis/需要特殊处理。通常情况下，应该交由nonGoRestfulMux
			// 但是在启用descovery的情况下，需要直接由goRestfulContainer处理
			if path == "/apis" || path == "/apis/" {
				klog.V(5).Infof("%v: %v %q satisfied by gorestful with webservice %v", d.name, req.Method, path, ws.RootPath())
				d.goRestfulContainer.Dispatch(w, req)
				return
			}
		// 如果前缀匹配
		case strings.HasPrefix(path, ws.RootPath()):
			if len(path) == len(ws.RootPath()) || path[len(ws.RootPath())] == '/' {
				klog.V(5).Infof("%v: %v %q satisfied by gorestful with webservice %v", d.name, req.Method, path, ws.RootPath())
				// don't use servemux here because gorestful servemuxes get messed up when removing webservices
				// TODO fix gorestful, remove TPRs, or stop using gorestful
				d.goRestfulContainer.Dispatch(w, req)
				return
			}
		}
	}

	// 无法找到匹配，跳过 gorestful 容器
	d.nonGoRestfulMux.ServeHTTP(w, req)
}</pre>
<p> 对于非API资源请求，由PathRecorderMux处理：</p>
<pre class="crayon-plain-tag">func (m *PathRecorderMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	m.mux.Load().(*pathHandler).ServeHTTP(w, r)
}
func (h *pathHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// pathToHandler 记录了所有精确匹配路径的处理器
	//   0 = /healthz/etcd -&gt; k8s.io/apiserver/pkg/server/healthz.adaptCheckToHandler.func1
	//   1 = /livez/etcd -&gt; k8s.io/apiserver/pkg/server/healthz.adaptCheckToHandler.func1
	//   2 = /metrics -&gt; k8s.io/apiserver/pkg/server/routes.MetricsWithReset.Install.func1
	//   3 = /readyz/shutdown -&gt; k8s.io/apiserver/pkg/server/healthz.adaptCheckToHandler.func1
	//   4 = /readyz/ping -&gt; k8s.io/apiserver/pkg/server/healthz.adaptCheckToHandler.func1
	//   5 = /debug/pprof/profile -&gt; net/http/pprof.Profile
	//   6 = /healthz/log -&gt; k8s.io/apiserver/pkg/server/healthz.adaptCheckToHandler.func1
	//   7 = /livez/log -&gt; k8s.io/apiserver/pkg/server/healthz.adaptCheckToHandler.func1
	//   8 = /healthz/ping -&gt; k8s.io/apiserver/pkg/server/healthz.adaptCheckToHandler.func1
	//   9 = / -&gt; 
	//   10 = /healthz -&gt; k8s.io/apiserver/pkg/endpoints/metrics.InstrumentHandlerFunc.func1
	//   11 = /debug/flags -&gt; k8s.io/apiserver/pkg/server/routes.DebugFlags.Index-fm
	//   12 = /livez/ping -&gt; k8s.io/apiserver/pkg/server/healthz.adaptCheckToHandler.func1
	//   13 = /readyz/log -&gt; k8s.io/apiserver/pkg/server/healthz.adaptCheckToHandler.func1
	//   14 = /debug/pprof -&gt; k8s.io/apiserver/pkg/server/routes.redirectTo.func1
	//   15 = /debug/pprof/trace -&gt; net/http/pprof.Trace
	//   16 = /debug/flags/v -&gt; k8s.io/apiserver/pkg/server/routes.StringFlagPutHandler.func1
	//   17 = /readyz/poststarthook/start-sample-server-informers -&gt; k8s.io/apiserver/pkg/server/healthz.adaptCheckToHandler.func1
	//   18 = /livez/poststarthook/start-sample-server-informers -&gt; k8s.io/apiserver/pkg/server/healthz.adaptCheckToHandler.func1
	//   19 = /openapi/v2 -&gt; github.com/NYTimes/gziphandler.NewGzipLevelAndMinSize.func1.1
	//   20 = /healthz/poststarthook/start-sample-server-informers -&gt; k8s.io/apiserver/pkg/server/healthz.adaptCheckToHandler.func1
	//   21 = /readyz/etcd -&gt; k8s.io/apiserver/pkg/server/healthz.adaptCheckToHandler.func1
	//   22 = /index.html -&gt; 
	//   23 = /debug/pprof/symbol -&gt; net/http/pprof.Symbol
	//   24 = /livez -&gt; k8s.io/apiserver/pkg/endpoints/metrics.InstrumentHandlerFunc.func1
	//   25 = /readyz -&gt; k8s.io/apiserver/pkg/endpoints/metrics.InstrumentHandlerFunc.func1
	if exactHandler, ok := h.pathToHandler[r.URL.Path]; ok {
		klog.V(5).Infof("%v: %q satisfied by exact match", h.muxName, r.URL.Path)
		exactHandler.ServeHTTP(w, r)
		return
	}

	// 前缀匹配路径的处理器，默认有/debug/flags/ 和 /debug/pprof/
	for _, prefixHandler := range h.prefixHandlers {
		if strings.HasPrefix(r.URL.Path, prefixHandler.prefix) {
			klog.V(5).Infof("%v: %q satisfied by prefix %v", h.muxName, r.URL.Path, prefixHandler.prefix)
			prefixHandler.handler.ServeHTTP(w, r)
			return
		}
	}

	// 找不到处理器，404
	klog.V(5).Infof("%v: %q satisfied by NotFoundHandler", h.muxName, r.URL.Path)
	h.notFoundHandler.ServeHTTP(w, r)
}</pre>
<p>对于API资源请求，例如路径 /apis/wardle.example.com/v1beta1，则由go-restful处理。go-restful框架内部处理细节我们就忽略了，我们主要深入探讨上文中提到的<a href="#InstallAPIGroup">安装APIGroup</a>，每种资源的go-restful Handler都是由它注册的：</p>
<pre class="crayon-plain-tag">func (s *GenericAPIServer) InstallAPIGroup(apiGroupInfo *APIGroupInfo) error {
	return s.InstallAPIGroups(apiGroupInfo)
}
func (s *GenericAPIServer) InstallAPIGroups(apiGroupInfos ...*APIGroupInfo) error {
	// 遍历API组
	for _, apiGroupInfo := range apiGroupInfos {
		//          安装API资源        常量 /apis
		if err := s.installAPIResources(APIGroupPrefix, apiGroupInfo, openAPIModels); err != nil {
			return fmt.Errorf("unable to install api resources: %v", err)
		}

		// setup discovery
		// Install the version handler.
		// Add a handler at /apis/&lt;groupName&gt; to enumerate all versions supported by this group.
		apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{}
		for _, groupVersion := range apiGroupInfo.PrioritizedVersions {
			// Check the config to make sure that we elide versions that don't have any resources
			if len(apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version]) == 0 {
				continue
			}
			apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{
				GroupVersion: groupVersion.String(),
				Version:      groupVersion.Version,
			})
		}
		preferredVersionForDiscovery := metav1.GroupVersionForDiscovery{
			GroupVersion: apiGroupInfo.PrioritizedVersions[0].String(),
			Version:      apiGroupInfo.PrioritizedVersions[0].Version,
		}
		apiGroup := metav1.APIGroup{
			Name:             apiGroupInfo.PrioritizedVersions[0].Group,
			Versions:         apiVersionsForDiscovery,
			PreferredVersion: preferredVersionForDiscovery,
		}

		s.DiscoveryGroupManager.AddGroup(apiGroup)
		s.Handler.GoRestfulContainer.Add(discovery.NewAPIGroupHandler(s.Serializer, apiGroup).WebService())
	}
	return nil
}

// 安装API资源
func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo, openAPIModels openapiproto.Models) error {
	// 遍历版本
	for _, groupVersion := range apiGroupInfo.PrioritizedVersions {
		// 跳过没有资源的组
		if len(apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version]) == 0 {
			klog.Warningf("Skipping API %v because it has no resources.", groupVersion)
			continue
		}

		apiGroupVersion := s.getAPIGroupVersion(apiGroupInfo, groupVersion, apiPrefix)
		if apiGroupInfo.OptionsExternalVersion != nil {
			apiGroupVersion.OptionsExternalVersion = apiGroupInfo.OptionsExternalVersion
		}
		apiGroupVersion.OpenAPIModels = openAPIModels
		apiGroupVersion.MaxRequestBodyBytes = s.maxRequestBodyBytes
		// 安装为go-restful的Handler
		if err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer); err != nil {
			return fmt.Errorf("unable to setup API %v: %v", apiGroupInfo, err)
		}
	}

	return nil
}

// 注册一系列REST Handler（ storage, watch, proxy, redirect）到restful容器
func (g *APIGroupVersion) InstallREST(container *restful.Container) error {
	// 例如/apis/wardle.example.com/v1beta1
	prefix := path.Join(g.Root, g.GroupVersion.Group, g.GroupVersion.Version)
	installer := &amp;APIInstaller{
		group:             g,
		prefix:            prefix,
		minRequestTimeout: g.MinRequestTimeout,
	}
	// 执行API资源处理器的安装
	apiResources, ws, registrationErrors := installer.Install()
	// 执行资源发现处理器的安装
	versionDiscoveryHandler := discovery.NewAPIVersionHandler(g.Serializer, g.GroupVersion, staticLister{apiResources})
	versionDiscoveryHandler.AddToWebService(ws)
	// 添加WebService到容器
	container.Add(ws)
	return utilerrors.NewAggregate(registrationErrors)
}
func (a *APIInstaller) Install() ([]metav1.APIResource, *restful.WebService, []error) {
	var apiResources []metav1.APIResource
	var errors []error
	// 创建一个针对特定GV的WebService
	ws := a.newWebService()

	// Register the paths in a deterministic (sorted) order to get a deterministic swagger spec.
	paths := make([]string, len(a.group.Storage))
	var i int = 0
	for path := range a.group.Storage {
		paths[i] = path
		i++
	}
	sort.Strings(paths)
	for _, path := range paths {
		// 遍历资源，注册处理器，例如flunders
		apiResource, err := a.registerResourceHandlers(path, a.group.Storage[path], ws)
		if err != nil {
			errors = append(errors, fmt.Errorf("error in registering resource: %s, %v", path, err))
		}
		if apiResource != nil {
			apiResources = append(apiResources, *apiResource)
		}
	}
	return apiResources, ws, errors
}
func (a *APIInstaller) newWebService() *restful.WebService {
	ws := new(restful.WebService)
	// 此WebService的路径模板
	ws.Path(a.prefix)
	// a.prefix contains "prefix/group/version"
	ws.Doc("API at " + a.prefix)
	// 向后兼容的考虑，支持没有MIME类型
	ws.Consumes("*/*")
	// 根据API组使用的编解码器来确定响应支持的MIME类型
	//   0 = {string} "application/json"
	//   1 = {string} "application/yaml"
	//   2 = {string} "application/vnd.kubernetes.protobuf"
	mediaTypes, streamMediaTypes := negotiation.MediaTypesForSerializer(a.group.Serializer)
	ws.Produces(append(mediaTypes, streamMediaTypes...)...)
	// 例如 wardle.example.com/v1beta1
	ws.ApiVersion(a.group.GroupVersion.String())
	return ws
}
// 注册资源处理器，过于冗长，仅仅贴片段
func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService) (*metav1.APIResource, error) {
	// ...
	// creater就是Handler的核心逻辑所在，它来自rest.Storage，对接Etcd
	creater, isCreater := storage.(rest.Creater)
	// ...
	actions = appendIf(actions, action{"POST", resourcePath, resourceParams, namer, false}, isCreater)
	switch action.Verb {
		case "POST": 
			handler = restfulCreateResource(creater, reqScope, admit)

			route := ws.POST(action.Path).To(handler).
				Doc(doc).
				Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
				Operation("create"+namespaced+kind+strings.Title(subresource)+operationSuffix).
				Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
				Returns(http.StatusOK, "OK", producedObject).
				// TODO: in some cases, the API may return a v1.Status instead of the versioned object
				// but currently go-restful can't handle multiple different objects being returned.
				Returns(http.StatusCreated, "Created", producedObject).
				Returns(http.StatusAccepted, "Accepted", producedObject).
				Reads(defaultVersionedObject).
				Writes(producedObject)
			if err := AddObjectParams(ws, route, versionedCreateOptions); err != nil {
				return nil, err
			}
			addParams(route, action.Params)
			routes = append(routes, route)
	// ...
}
func restfulCreateResource(r rest.Creater, scope handlers.RequestScope, admit admission.Interface) restful.RouteFunction {
	return func(req *restful.Request, res *restful.Response) {
		handlers.CreateResource(r, &amp;scope, admit)(res.ResponseWriter, req.Request)
	}
}
func CreateResource(r rest.Creater, scope *RequestScope, admission admission.Interface) http.HandlerFunc {
	return createHandler(&amp;namedCreaterAdapter{r}, scope, admission, false)
}
// 创建资源处理器核心
func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Interface, includeName bool) http.HandlerFunc {
	return func(w http.ResponseWriter, req *http.Request) {
		// For performance tracking purposes.
		trace := utiltrace.New("Create", utiltrace.Field{Key: "url", Value: req.URL.Path}, utiltrace.Field{Key: "user-agent", Value: &amp;lazyTruncatedUserAgent{req}}, utiltrace.Field{Key: "client", Value: &amp;lazyClientIP{req}})
		defer trace.LogIfLong(500 * time.Millisecond)
		// 处理Dryrun
		if isDryRun(req.URL) &amp;&amp; !utilfeature.DefaultFeatureGate.Enabled(features.DryRun) {
			scope.err(errors.NewBadRequest("the dryRun feature is disabled"), w, req)
			return
		}

		// TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer)
		timeout := parseTimeout(req.URL.Query().Get("timeout"))
		// 命名空间和资源名字处理
		namespace, name, err := scope.Namer.Name(req)
		if err != nil {
			if includeName {
				// name was required, return
				scope.err(err, w, req)
				return
			}

			// otherwise attempt to look up the namespace
			namespace, err = scope.Namer.Namespace(req)
			if err != nil {
				scope.err(err, w, req)
				return
			}
		}

		ctx, cancel := context.WithTimeout(req.Context(), timeout)
		defer cancel()
		ctx = request.WithNamespace(ctx, namespace)
		// 协商输出MIME类型
		outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, scope)
		if err != nil {
			scope.err(err, w, req)
			return
		}

		gv := scope.Kind.GroupVersion()
		// 协商输入如何反串行化
		s, err := negotiation.NegotiateInputSerializer(req, false, scope.Serializer)
		if err != nil {
			scope.err(err, w, req)
			return
		}
		// 从串行化器得到能将请求解码为特定版本的解码器
		decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion)

		// 读取请求体
		body, err := limitedReadBody(req, scope.MaxRequestBodyBytes)
		if err != nil {
			scope.err(err, w, req)
			return
		}

		options := &amp;metav1.CreateOptions{}
		values := req.URL.Query()
		if err := metainternalversionscheme.ParameterCodec.DecodeParameters(values, scope.MetaGroupVersion, options); err != nil {
			err = errors.NewBadRequest(err.Error())
			scope.err(err, w, req)
			return
		}
		if errs := validation.ValidateCreateOptions(options); len(errs) &gt; 0 {
			err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "CreateOptions"}, "", errs)
			scope.err(err, w, req)
			return
		}
		options.TypeMeta.SetGroupVersionKind(metav1.SchemeGroupVersion.WithKind("CreateOptions"))

		defaultGVK := scope.Kind
		// 实例化资源的Go结构
		original := r.New()
		trace.Step("About to convert to expected version")
		// 将请求体解码到Go结构中
		obj, gvk, err := decoder.Decode(body, &amp;defaultGVK, original)
		if err != nil {
			err = transformDecodeError(scope.Typer, err, original, gvk, body)
			scope.err(err, w, req)
			return
		}
		if gvk.GroupVersion() != gv {
			err = errors.NewBadRequest(fmt.Sprintf("the API version in the data (%s) does not match the expected API version (%v)", gvk.GroupVersion().String(), gv.String()))
			scope.err(err, w, req)
			return
		}
		trace.Step("Conversion done")

		// 审计和Admission控制
		ae := request.AuditEventFrom(ctx)
		admit = admission.WithAudit(admit, ae)
		audit.LogRequestObject(ae, obj, scope.Resource, scope.Subresource, scope.Serializer)

		userInfo, _ := request.UserFrom(ctx)

		// On create, get name from new object if unset
		if len(name) == 0 {
			_, name, _ = scope.Namer.ObjectName(obj)
		}

		trace.Step("About to store object in database")
		admissionAttributes := admission.NewAttributesRecord(obj, nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Create, options, dryrun.IsDryRun(options.DryRun), userInfo)
		// 构建入库函数
		requestFunc := func() (runtime.Object, error) {
			// 调用rest.Storage进行入库
			return r.Create(
				ctx,
				name,
				obj,
				rest.AdmissionToValidateObjectFunc(admit, admissionAttributes, scope),
				options,
			)
		}
		// finishRequest能够异步执行回调，并且处理响应返回的错误
		result, err := finishRequest(timeout, func() (runtime.Object, error) {
			if scope.FieldManager != nil {
				liveObj, err := scope.Creater.New(scope.Kind)
				if err != nil {
					return nil, fmt.Errorf("failed to create new object (Create for %v): %v", scope.Kind, err)
				}
				obj = scope.FieldManager.UpdateNoErrors(liveObj, obj, managerOrUserAgent(options.FieldManager, req.UserAgent()))
			}
			if mutatingAdmission, ok := admit.(admission.MutationInterface); ok &amp;&amp; mutatingAdmission.Handles(admission.Create) {
				if err := mutatingAdmission.Admit(ctx, admissionAttributes, scope); err != nil {
					return nil, err
				}
			}
			result, err := requestFunc()
			// If the object wasn't committed to storage because it's serialized size was too large,
			// it is safe to remove managedFields (which can be large) and try again.
			if isTooLargeError(err) {
				if accessor, accessorErr := meta.Accessor(obj); accessorErr == nil {
					accessor.SetManagedFields(nil)
					result, err = requestFunc()
				}
			}
			return result, err
		})
		if err != nil {
			scope.err(err, w, req)
			return
		}
		trace.Step("Object stored in database")

		code := http.StatusCreated
		status, ok := result.(*metav1.Status)
		if ok &amp;&amp; err == nil &amp;&amp; status.Code == 0 {
			status.Code = int32(code)
		}

		transformResponseObject(ctx, scope, trace, req, w, code, outputMediaType, result)
	}
}</pre>
<p>回顾一下请求处理的整体逻辑：</p>
<ol style="list-style-type: undefined;">
<li>GenericAPIServer.Handler就是http.Handler，可以注册给任何HTTP服务器。因此我们想绕开HTTPS的限制应该很容易</li>
<li>GenericAPIServer.Handler是一个层层包裹的处理器链，外层是一系列过滤器，最里面是director</li>
<li>director负责整体的请求分发：
<ol>
<li>对于非API资源请求，分发给nonGoRestfulMux。我们可以利用这个扩展点，扩展任意形式的HTTP接口</li>
<li>对于API资源请求，分发给gorestfulContainer</li>
</ol>
</li>
<li>在GenericAPIServer.InstallAPIGroup中，所有支持的API资源的所有版本，都注册为go-restful的一个WebService</li>
<li>这些WebService的逻辑包括（依赖于rest.Storage）：
<ol>
<li>将请求解码为资源对应的Go结构</li>
<li>将Go结构编码为JSON</li>
<li>将JSON存储到Etcd</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">sample-apiserver小结</span></div>
<p>通过对sample-apiserver的代码分析，我们理解了构建自己的API Server的各种关键要素。</p>
<p>APIServer的核心类型是<pre class="crayon-plain-tag">GenericAPIServer</pre>，它是由<pre class="crayon-plain-tag">genericapiserver.CompletedConfig</pre>的<pre class="crayon-plain-tag">New()</pre>方法生成的。后者则是<pre class="crayon-plain-tag">genericapiserver.RecommendedConfig</pre>的<pre class="crayon-plain-tag">Complete()</pre>方法生成的。而RecommendedConfig又是从<pre class="crayon-plain-tag">genericoptions.RecommendedOptions</pre>得到的。sample-apiserver对Config、Option、Server等对象都做了一层包装，我们不关注这些wrapper。</p>
<p>RecommendedOptions对应了用户提供的各类选项（外加所谓推荐选项，降低使用时的复杂度），例如Etcd地址、Etcd存储前缀、APIServer的基本信息等等。调用RecommendedOptions的<pre class="crayon-plain-tag">ApplyTo</pre>方法，会根据选项，推导出APIServer所需的，完整的配置信息。在这个方法中，甚至会进行自签名证书等重操作，而不是简单的将信息从Option复制给Config。RecommendedOptions会依次调用它的各个字段的ApplyTo方法，从而推导出RecommendedConfig的各个字段。</p>
<p>RecommendedConfig的Complete方法，再一次进行配置信息的推导，主要牵涉到OpenAPI相关的配置。</p>
<p>CompletedConfig的New方法实例化GenericAPIServer，这一步最关键的逻辑是安装API组。API组定义了如何实现GroupVersion中API的增删改查，它将GroupVersion的每种资源映射到registry.REST，后者具有处理REST风格请求的能力，并（默认）存储到Etcd。</p>
<p>GenericAPIServer提供了一些钩子来处理Admission控制器的注册、初始化。以及另外一些钩子来对API Server的生命周期事件做出响应。</p>
<div class="blog_h2"><span class="graybg">sample-apiserver改造</span></div>
<div class="blog_h3"><span class="graybg">解除对kube-apiserver的依赖</span></div>
<p>想实现sample-apiserver的独立运行，RecommendedOptions有三个字段必须处理：Authentication、Authorization、CoreAPI，它们都隐含了对主kube-apiserver的依赖。</p>
<p>Authentication依赖主kube-apiserver，是因为它需要访问TokenReviewInterface，访问kube-system中的ConfigMap。Authorization依赖主kube-apiserver，是因为它需要访问SubjectAccessReviewInterface。CoreAPI则是直接为Config提供了两个字段：ClientConfig、SharedInformerFactory。</p>
<p>将这些字段置空，可以解除对主kube-apiserver的依赖。这样启动sample-apiserver时就不需要提供这三个命令行选项：</p>
<p style="padding-left: 30px;">--kubeconfig=/home/alex/.kube/config<br />--authentication-kubeconfig=/home/alex/.kube/config<br />--authorization-kubeconfig=/home/alex/.kube/config</p>
<p>但是，置空CoreAPI会导致报错：admission depends on a Kubernetes core API shared informer, it cannot be nil。这提示我们不能在不依赖主kube-apiserver的情况下使用Admission控制器这一特性，需要将Admission也置空：</p>
<pre class="crayon-plain-tag">o.RecommendedOptions.Authentication = nil
o.RecommendedOptions.Authorization = nil
o.RecommendedOptions.CoreAPI = nil
o.RecommendedOptions.Admission = nil</pre>
<p>清空上述四个字段后，sample-server还会在PostStart钩子中崩溃：</p>
<pre class="crayon-plain-tag">// panic，这个SharedInformerFactory是CoreAPI选项提供的
config.GenericConfig.SharedInformerFactory.Start(context.StopCh)
// 仅仅Admission控制器使用该InformerFactory
o.SharedInformerFactory.Start(context.StopCh)</pre>
<p>由于注释中给出的原因，这个PostStart钩子已经没有意义，删除即可正常启动服务器。</p>
<div class="blog_h3"><span class="graybg">使用HTTP而非HTTPS</span></div>
<p>GenericAPIServer的<pre class="crayon-plain-tag">Run</pre>方法的默认实现，是调用<pre class="crayon-plain-tag">s.SecureServingInfo.Serve</pre>，因而强制使用HTTPS：</p>
<pre class="crayon-plain-tag">stoppedCh, err = s.SecureServingInfo.Serve(s.Handler, s.ShutdownTimeout, internalStopCh)</pre>
<p>不过，很明显的，我们只需要将s.Handler传递给自己的http.Server即可使用HTTP。</p>
<div class="blog_h3"><span class="graybg">添加任意HTTP接口</span></div>
<p>我们的迁移工具还提供一些非Kubernetes风格的HTTP接口，那么如何集成到APIServer中呢？</p>
<p>在启动服务器之前，可以直接访问<pre class="crayon-plain-tag">GenericAPIServer.Handler.NonGoRestfulMux</pre>，NonGoRestfulMux实现了：</p>
<pre class="crayon-plain-tag">type mux interface {
	Handle(pattern string, handler http.Handler)
}</pre>
<p>调用Handle即可为任何路径注册处理器。</p>
<div class="blog_h1"><span class="graybg">apiserver-builder-alpha</span></div>
<p>通过对sample-apiserver代码的分析，我们了解到构建自己的API Server有大量繁琐的工作需要做。幸运的是，K8S提供了<a href="https://github.com/kubernetes-sigs/apiserver-builder-alpha">apiserver-builder-alpha</a>简化这一过程。</p>
<p>apiserver-builder-alpha是一系列工具和库的集合，它能够：</p>
<ol>
<li>为新的API资源创建Go类型、控制器（基于controller-runtime）、测试用例、文档</li>
<li>构建、（独立、在Minikube或者在K8S中）运行扩展的控制平面组件（APIServer）</li>
<li>让在控制器中watch/update资源更简单</li>
<li>让创建新的资源/子资源更简单</li>
<li>提供大部分合理的默认值</li>
</ol>
<div class="blog_h2"><span class="graybg">安装</span></div>
<p>下载<a href="https://github.com/kubernetes-sigs/apiserver-builder-alpha/releases">压缩包</a>，解压并存放到目录，然后设置环境变量：</p>
<pre class="crayon-plain-tag">export PATH=$HOME/.local/kubernetes/apiserver-builder/bin/:$PATH</pre>
<div class="blog_h2"><span class="graybg">起步</span></div>
<p>你需要在$GOPATH下创建一个项目，创建一个boilerplate.go.txt文件。然后执行：</p>
<pre class="crayon-plain-tag">apiserver-boot init repo --domain cloud.gmem.cc</pre>
<p>该命令会生成如下目录结构：</p>
<pre class="crayon-plain-tag">.
├── bin
├── boilerplate.go.txt
├── BUILD.bazel
├── cmd
│   ├── apiserver
│   │   └── main.go
│   └── manager
│       └── main.go
├── go.mod
├── go.sum
├── pkg
│   ├── apis
│   │   └── doc.go
│   ├── controller
│   │   └── doc.go
│   ├── doc.go
│   ├── openapi
│   │   └── doc.go
│   └── webhook
│       └── webhook.go
├── PROJECT
└── WORKSPAC</pre>
<p> cmd/apiserver/main.go，是APIServer的入口点：</p>
<pre class="crayon-plain-tag">import "sigs.k8s.io/apiserver-builder-alpha/pkg/cmd/server"

func main() {
	version := "v0"

	err := server.StartApiServerWithOptions(&amp;server.StartOptions{
		EtcdPath:         "/registry/cloud.gmem.cc",
		//                无法运行，这个函数不存在
		Apis:             apis.GetAllApiBuilders(),
		Openapidefs:      openapi.GetOpenAPIDefinitions,
		Title:            "Api",
		Version:          version,

		// TweakConfigFuncs []func(apiServer *apiserver.Config) error
		// FlagConfigFuncs []func(*cobra.Command) error
	})
	if err != nil {
		panic(err)
	}
}</pre>
<p>可以看到apiserver-builder-alpha进行了一些封装。</p>
<p>执行下面的命令，添加一个新的API资源：</p>
<pre class="crayon-plain-tag">apiserver-boot create group version resource --group tcm --version v1 --kind Flunder</pre>
<p>最后，执行命令可以在本地启动APIServer：</p>
<pre class="crayon-plain-tag">apiserver-boot run local</pre>
<div class="blog_h2"><span class="graybg">问题 </span></div>
<p>本文提及的工具项目，最初架构是基于CRD，使用kubebuilder进行代码生成。kubebuilder的目录结构和apiserver-builder并不兼容。</p>
<p>此外apiserver-builder项目仍然处于Alpha阶段，并且经过测试，发现生成代码无法运行。为了避免不必要的麻烦，我们不打算使用它。</p>
<div class="blog_h1"><span class="graybg">编写APIServer</span></div>
<p>由于apiserver-builder不成熟，而且我们已经基于kubebuilder完成了大部分开发工作。因此打算基于分析sample-apiserver获得的经验，手工编写一个独立运行、使用HTTP协议的APIServer。</p>
<p>kubebuilder并不会生成zz_generated.openapi.go文件，因为该文件对于CRD没有意义。但是这个文件对于独立API Server是必须的。</p>
<p>我们需要为资源类型所在包添加注解：</p>
<pre class="crayon-plain-tag">// +k8s:openapi-gen=true

package v1</pre>
<p>并调用<a href="https://blog.gmem.cc/openapi#openapi-gen">openapi-gen</a>生成此文件：</p>
<pre class="crayon-plain-tag">openapi-gen  \
	--input-dirs "k8s.io/apimachinery/pkg/apis/meta/v1,k8s.io/apimachinery/pkg/runtime,k8s.io/apimachinery/pkg/version" \
	--input-dirs cloud.gmem.cc/teleport/api/v1    -p cloud.gmem.cc/teleport/api/v1 -O zz_generated.openapi</pre>
<p>下面是完整的quick&amp;dirty的代码：</p>
<pre class="crayon-plain-tag">package main

import (
	v1 "cloud.gmem.cc/teleport/api/v1"
	"context"
	"fmt"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/runtime/serializer"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/apimachinery/pkg/util/validation/field"
	"k8s.io/apiserver/pkg/endpoints/openapi"
	"k8s.io/apiserver/pkg/features"
	"k8s.io/apiserver/pkg/registry/generic"
	genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
	"k8s.io/apiserver/pkg/registry/rest"
	genericapiserver "k8s.io/apiserver/pkg/server"
	genericoptions "k8s.io/apiserver/pkg/server/options"
	"k8s.io/apiserver/pkg/storage"
	"k8s.io/apiserver/pkg/storage/names"
	"k8s.io/apiserver/pkg/util/feature"
	utilfeature "k8s.io/apiserver/pkg/util/feature"
	"net"
	"net/http"
	"os"
	"reflect"
	ctrl "sigs.k8s.io/controller-runtime"
)

var (
	setupLog = ctrl.Log.WithName("setup")
)

func main() {

	s := runtime.NewScheme()
	utilruntime.Must(v1.AddToScheme(s))
	gv := v1.GroupVersion
	utilruntime.Must(s.SetVersionPriority(gv))
	metav1.AddToGroupVersion(s, schema.GroupVersion{Version: "v1"})
	unversioned := schema.GroupVersion{Group: "", Version: "v1"}
	s.AddUnversionedTypes(unversioned,
		&amp;metav1.Status{},
		&amp;metav1.APIVersions{},
		&amp;metav1.APIGroupList{},
		&amp;metav1.APIGroup{},
		&amp;metav1.APIResourceList{},
	)

	//  必须注册一个__internal版本，否则报错
	//  failed to prepare current and previous objects: no kind "Flunder" is registered for the internal version of group "tcm.cloud.gmem.cc" in scheme 
	gvi := gv
	gvi.Version = runtime.APIVersionInternal
	s.AddKnownTypes(gvi, &amp;v1.Flunder{}, &amp;v1.FlunderList{})

	codecFactory := serializer.NewCodecFactory(s)
	codec := codecFactory.LegacyCodec(gv)
	options := genericoptions.NewRecommendedOptions(
		"/teleport/cloud.gmem.cc",
		codec,
	)
	options.Etcd.StorageConfig.EncodeVersioner = runtime.NewMultiGroupVersioner(gv, schema.GroupKind{Group: gv.Group})
	ips := []net.IP{net.ParseIP("127.0.0.1")}
	if err := options.SecureServing.MaybeDefaultWithSelfSignedCerts("localhost", nil, ips); err != nil {
		setupLog.Error(err, "error creating self-signed certificates")
		os.Exit(1)
	}
	options.Etcd.StorageConfig.Paging = utilfeature.DefaultFeatureGate.Enabled(features.APIListChunking)
	options.Etcd.StorageConfig.Transport.ServerList = []string{"http://etcd.gmem.cc:2379"}

	options.Authentication = nil
	options.Authorization = nil
	options.CoreAPI = nil
	options.Admission = nil
	options.SecureServing.BindPort = 6443

	config := genericapiserver.NewRecommendedConfig(codecFactory)
	config.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(v1.GetOpenAPIDefinitions,
		openapi.NewDefinitionNamer(s))
	config.OpenAPIConfig.Info.Title = "Teleport"
	config.OpenAPIConfig.Info.Version = "1.0"

	feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{
		string(features.APIPriorityAndFairness): false,
	})

	if err := options.ApplyTo(config); err != nil {
		panic(err)
	}
	completedConfig := config.Complete()
	server, err := completedConfig.New("teleport-apiserver", genericapiserver.NewEmptyDelegate())
	if err != nil {
		panic(err)
	}

	apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(gv.Group, s, metav1.ParameterCodec, codecFactory)
	v1storage := map[string]rest.Storage{}
	resource := v1.ResourceFlunders
	v1storage[resource] = createStore(
		s,
		gv.WithResource(resource).GroupResource(),
		func() runtime.Object { return &amp;v1.Flunder{} },
		func() runtime.Object { return &amp;v1.FlunderList{} },
		completedConfig.RESTOptionsGetter,
	)
	apiGroupInfo.VersionedResourcesStorageMap[gv.Version] = v1storage
	if err := server.InstallAPIGroups(&amp;apiGroupInfo); err != nil {
		panic(err)
	}
	server.AddPostStartHookOrDie("teleport-post-start", func(context genericapiserver.PostStartHookContext) error {
		return nil
	})
	preparedServer := server.PrepareRun()
	http.ListenAndServe(":6080", preparedServer.Handler)
}

func createStore(scheme *runtime.Scheme, gr schema.GroupResource, newFunc, newListFunc func() runtime.Object,
	optsGetter generic.RESTOptionsGetter) rest.Storage {
	attrs := func(obj runtime.Object) (labels.Set, fields.Set, error) {
		typ := reflect.TypeOf(newFunc())
		if reflect.TypeOf(obj) != typ {
			return nil, nil, fmt.Errorf("given object is not a %s", typ.Name())
		}
		oma := obj.(metav1.ObjectMetaAccessor)
		meta := oma.GetObjectMeta()
		return meta.GetLabels(), fields.Set{
			"metadata.name":      meta.GetName(),
			"metadata.namespace": meta.GetNamespace(),
		}, nil
	}
	s := strategy{
		scheme,
		names.SimpleNameGenerator,
	}
	store := &amp;genericregistry.Store{
		NewFunc:     newFunc,
		NewListFunc: newListFunc,
		PredicateFunc: func(label labels.Selector, field fields.Selector) storage.SelectionPredicate {
			return storage.SelectionPredicate{
				Label:    label,
				Field:    field,
				GetAttrs: attrs,
			}
		},
		DefaultQualifiedResource: gr,

		CreateStrategy: s,
		UpdateStrategy: s,
		DeleteStrategy: s,

		TableConvertor: rest.NewDefaultTableConvertor(gr),
	}
	options := &amp;generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: attrs}
	if err := store.CompleteWithOptions(options); err != nil {
		panic(err)
	}
	return store
}

type strategy struct {
	runtime.ObjectTyper
	names.NameGenerator
}

func (strategy) NamespaceScoped() bool {
	return true
}

func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
}

func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
}

func (strategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
	return field.ErrorList{}
}

func (strategy) AllowCreateOnUpdate() bool {
	return false
}

func (strategy) AllowUnconditionalUpdate() bool {
	return false
}

func (strategy) Canonicalize(obj runtime.Object) {
}

func (strategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
	return field.ErrorList{}
}</pre>
<div class="blog_h1"><span class="graybg">定制存储后端 </span></div>
<p>在安装APIGroup的时候，我们需要为每API组的每个版本的每种资源，指定存储后端：</p>
<pre class="crayon-plain-tag">// 每个组
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(wardle.GroupName, Scheme, metav1.ParameterCodec, Codecs)
// 每个版本
v1alpha1storage := map[stcongring]rest.Storage{}
// 每种资源提供一个rest.Storage
v1alpha1storage["flunders"] = wardleregistry.RESTInPeace(flunderstorage.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter))

// 安装APIGroup
s.GenericAPIServer.InstallAPIGroup(&amp;apiGroupInfo)</pre>
<p>默认情况下，使用的是genericregistry.Store，它对接到Etcd。要实现自己的存储后端，实现相关接口即可。</p>
<p>注意：关于存储后端，有很多细节需要处理。</p>
<div class="blog_h2"><span class="graybg">基于文件的存储</span></div>
<p>下面贴一个在文件系统中，以YAML形式存储API资源的例子：</p>
<pre class="crayon-plain-tag">package file

import (
	"bytes"
	"context"
	"fmt"
	"io/ioutil"
	"k8s.io/apimachinery/pkg/util/uuid"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"sync"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/conversion"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/watch"
	genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
	"k8s.io/apiserver/pkg/registry/rest"
)

var _ rest.StandardStorage = &amp;store{}
var _ rest.Scoper = &amp;store{}
var _ rest.Storage = &amp;store{}

// NewStore instantiates a new file storage
func NewStore(groupResource schema.GroupResource, codec runtime.Codec, rootpath string, isNamespaced bool,
	newFunc func() runtime.Object, newListFunc func() runtime.Object, tc rest.TableConvertor) rest.Storage {
	objRoot := filepath.Join(rootpath, groupResource.Group, groupResource.Resource)
	if err := ensureDir(objRoot); err != nil {
		panic(fmt.Sprintf("unable to write data dir: %s", err))
	}
	rest := &amp;store{
		defaultQualifiedResource: groupResource,
		TableConvertor:           tc,
		codec:                    codec,
		objRootPath:              objRoot,
		isNamespaced:             isNamespaced,
		newFunc:                  newFunc,
		newListFunc:              newListFunc,
		watchers:                 make(map[int]*yamlWatch, 10),
	}
	return rest
}

type store struct {
	rest.TableConvertor
	codec        runtime.Codec
	objRootPath  string
	isNamespaced bool

	muWatchers sync.RWMutex
	watchers   map[int]*yamlWatch

	newFunc                  func() runtime.Object
	newListFunc              func() runtime.Object
	defaultQualifiedResource schema.GroupResource
}

func (f *store) notifyWatchers(ev watch.Event) {
	f.muWatchers.RLock()
	for _, w := range f.watchers {
		w.ch &lt;- ev
	}
	f.muWatchers.RUnlock()
}

func (f *store) New() runtime.Object {
	return f.newFunc()
}

func (f *store) NewList() runtime.Object {
	return f.newListFunc()
}

func (f *store) NamespaceScoped() bool {
	return f.isNamespaced
}

func (f *store) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
	return read(f.codec, f.objectFileName(ctx, name), f.newFunc)
}

func (f *store) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
	newListObj := f.NewList()
	v, err := getListPrt(newListObj)
	if err != nil {
		return nil, err
	}

	dirname := f.objectDirName(ctx)
	if err := visitDir(dirname, f.newFunc, f.codec, func(path string, obj runtime.Object) {
		appendItem(v, obj)
	}); err != nil {
		return nil, fmt.Errorf("failed walking filepath %v", dirname)
	}
	return newListObj, nil
}

func (f *store) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc,
	options *metav1.CreateOptions) (runtime.Object, error) {
	if createValidation != nil {
		if err := createValidation(ctx, obj); err != nil {
			return nil, err
		}
	}
	if f.isNamespaced {
		ns, ok := genericapirequest.NamespaceFrom(ctx)
		if !ok {
			return nil, apierrors.NewBadRequest("namespace required")
		}
		if err := ensureDir(filepath.Join(f.objRootPath, ns)); err != nil {
			return nil, err
		}
	}

	accessor, err := meta.Accessor(obj)
	if err != nil {
		return nil, err
	}
	if accessor.GetUID() == "" {
		accessor.SetUID(uuid.NewUUID())
	}

	name := accessor.GetName()
	filename := f.objectFileName(ctx, name)
	qualifiedResource := f.qualifiedResourceFromContext(ctx)
	if exists(filename) {
		return nil, apierrors.NewAlreadyExists(qualifiedResource, name)
	}

	if err := write(f.codec, filename, obj); err != nil {
		return nil, apierrors.NewInternalError(err)
	}

	f.notifyWatchers(watch.Event{
		Type:   watch.Added,
		Object: obj,
	})

	return obj, nil
}

func (f *store) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo,
	createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc,
	forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
	isCreate := false
	oldObj, err := f.Get(ctx, name, nil)
	if err != nil {
		if !forceAllowCreate {
			return nil, false, err
		}
		isCreate = true
	}

	if f.isNamespaced {
		// ensures namespace dir
		ns, ok := genericapirequest.NamespaceFrom(ctx)
		if !ok {
			return nil, false, apierrors.NewBadRequest("namespace required")
		}
		if err := ensureDir(filepath.Join(f.objRootPath, ns)); err != nil {
			return nil, false, err
		}
	}

	updatedObj, err := objInfo.UpdatedObject(ctx, oldObj)
	if err != nil {
		return nil, false, err
	}
	filename := f.objectFileName(ctx, name)

	if isCreate {
		if createValidation != nil {
			if err := createValidation(ctx, updatedObj); err != nil {
				return nil, false, err
			}
		}
		if err := write(f.codec, filename, updatedObj); err != nil {
			return nil, false, err
		}
		f.notifyWatchers(watch.Event{
			Type:   watch.Added,
			Object: updatedObj,
		})
		return updatedObj, true, nil
	}

	if updateValidation != nil {
		if err := updateValidation(ctx, updatedObj, oldObj); err != nil {
			return nil, false, err
		}
	}
	if err := write(f.codec, filename, updatedObj); err != nil {
		return nil, false, err
	}
	f.notifyWatchers(watch.Event{
		Type:   watch.Modified,
		Object: updatedObj,
	})
	return updatedObj, false, nil
}

func (f *store) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc,
	options *metav1.DeleteOptions) (runtime.Object, bool, error) {
	filename := f.objectFileName(ctx, name)
	qualifiedResource := f.qualifiedResourceFromContext(ctx)
	if !exists(filename) {
		return nil, false, apierrors.NewNotFound(qualifiedResource, name)
	}

	oldObj, err := f.Get(ctx, name, nil)
	if err != nil {
		return nil, false, err
	}
	if deleteValidation != nil {
		if err := deleteValidation(ctx, oldObj); err != nil {
			return nil, false, err
		}
	}

	if err := os.Remove(filename); err != nil {
		return nil, false, err
	}
	f.notifyWatchers(watch.Event{
		Type:   watch.Deleted,
		Object: oldObj,
	})
	return oldObj, true, nil
}

func (f *store) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc,
	options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) {
	newListObj := f.NewList()
	v, err := getListPrt(newListObj)
	if err != nil {
		return nil, err
	}
	dirname := f.objectDirName(ctx)
	if err := visitDir(dirname, f.newFunc, f.codec, func(path string, obj runtime.Object) {
		_ = os.Remove(path)
		appendItem(v, obj)
	}); err != nil {
		return nil, fmt.Errorf("failed walking filepath %v", dirname)
	}
	return newListObj, nil
}

func (f *store) objectFileName(ctx context.Context, name string) string {
	if f.isNamespaced {
		// FIXME: return error if namespace is not found
		ns, _ := genericapirequest.NamespaceFrom(ctx)
		return filepath.Join(f.objRootPath, ns, name+".yaml")
	}
	return filepath.Join(f.objRootPath, name+".yaml")
}

func (f *store) objectDirName(ctx context.Context) string {
	if f.isNamespaced {
		// FIXME: return error if namespace is not found
		ns, _ := genericapirequest.NamespaceFrom(ctx)
		return filepath.Join(f.objRootPath, ns)
	}
	return filepath.Join(f.objRootPath)
}

func write(encoder runtime.Encoder, filepath string, obj runtime.Object) error {
	buf := new(bytes.Buffer)
	if err := encoder.Encode(obj, buf); err != nil {
		return err
	}
	return ioutil.WriteFile(filepath, buf.Bytes(), 0600)
}

func read(decoder runtime.Decoder, path string, newFunc func() runtime.Object) (runtime.Object, error) {
	content, err := ioutil.ReadFile(filepath.Clean(path))
	if err != nil {
		return nil, err
	}
	newObj := newFunc()
	decodedObj, _, err := decoder.Decode(content, nil, newObj)
	if err != nil {
		return nil, err
	}
	return decodedObj, nil
}

func exists(filepath string) bool {
	_, err := os.Stat(filepath)
	return err == nil
}

func ensureDir(dirname string) error {
	if !exists(dirname) {
		return os.MkdirAll(dirname, 0700)
	}
	return nil
}

func visitDir(dirname string, newFunc func() runtime.Object, codec runtime.Decoder,
	visitFunc func(string, runtime.Object)) error {
	return filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			return nil
		}
		if !strings.HasSuffix(info.Name(), ".yaml") {
			return nil
		}
		newObj, err := read(codec, path, newFunc)
		if err != nil {
			return err
		}
		visitFunc(path, newObj)
		return nil
	})
}

func appendItem(v reflect.Value, obj runtime.Object) {
	v.Set(reflect.Append(v, reflect.ValueOf(obj).Elem()))
}

func getListPrt(listObj runtime.Object) (reflect.Value, error) {
	listPtr, err := meta.GetItemsPtr(listObj)
	if err != nil {
		return reflect.Value{}, err
	}
	v, err := conversion.EnforcePtr(listPtr)
	if err != nil || v.Kind() != reflect.Slice {
		return reflect.Value{}, fmt.Errorf("need ptr to slice: %v", err)
	}
	return v, nil
}

func (f *store) Watch(ctx context.Context, options *metainternalversion.ListOptions) (watch.Interface, error) {
	yw := &amp;yamlWatch{
		id: len(f.watchers),
		f:  f,
		ch: make(chan watch.Event, 10),
	}
	// On initial watch, send all the existing objects
	list, err := f.List(ctx, options)
	if err != nil {
		return nil, err
	}

	danger := reflect.ValueOf(list).Elem()
	items := danger.FieldByName("Items")

	for i := 0; i &lt; items.Len(); i++ {
		obj := items.Index(i).Addr().Interface().(runtime.Object)
		yw.ch &lt;- watch.Event{
			Type:   watch.Added,
			Object: obj,
		}
	}

	f.muWatchers.Lock()
	f.watchers[yw.id] = yw
	f.muWatchers.Unlock()

	return yw, nil
}

type yamlWatch struct {
	f  *store
	id int
	ch chan watch.Event
}

func (w *yamlWatch) Stop() {
	w.f.muWatchers.Lock()
	delete(w.f.watchers, w.id)
	w.f.muWatchers.Unlock()
}

func (w *yamlWatch) ResultChan() &lt;-chan watch.Event {
	return w.ch
}

func (f *store) ConvertToTable(ctx context.Context, object runtime.Object,
	tableOptions runtime.Object) (*metav1.Table, error) {
	return f.TableConvertor.ConvertToTable(ctx, object, tableOptions)
}
func (f *store) qualifiedResourceFromContext(ctx context.Context) schema.GroupResource {
	if info, ok := genericapirequest.RequestInfoFrom(ctx); ok {
		return schema.GroupResource{Group: info.APIGroup, Resource: info.Resource}
	}
	// some implementations access storage directly and thus the context has no RequestInfo
	return f.defaultQualifiedResource
}</pre>
<p>调用NewStore即可创建一个rest.Storage。前面我们提到过存储后端有很多细节需要处理，对于上面这个样例，它没有：</p>
<ol>
<li>发现正在删除中的资源，并在CRUD时作出适当响应</li>
<li>进行资源合法性校验。genericregistry.Store的做法是，调用strategy进行校验</li>
<li>自动填充某些元数据字段，包括creationTimestamp、selfLink等</li>
</ol>
<div class="blog_h1"><span class="graybg">处理子资源</span></div>
<p>假如K8S中某种资源具有状态子资源。那么当客户端更新状态子资源时，发出的HTTP请求格式为：</p>
<p style="padding-left: 30px;">PUT /apis/cloud.gmem.cc/v1/namespaces/default/flunders/sample/status</p>
<p>它会匹配路由：</p>
<p style="padding-left: 30px;">PUT /apis/cloud.gmem.cc/v1/namespaces/{namespace}/flunders/{name}/status</p>
<p>这个路由是专门为status子资源准备的，和主资源路由不同：</p>
<p style="padding-left: 30px;">PUT /apis/cloud.gmem.cc/v1/namespaces/{namespace}/flunders/{name}</p>
<p>那么，主资源、子资源的处理方式有什么不同？如何影响这种资源处理逻辑呢？</p>
<div class="blog_h2"><span class="graybg">注册子资源</span></div>
<p>InstallAPIGroups时，你只需要简单的为带有 / 的资源名字符串添加一个rest.Storage，就支持子资源了：</p>
<pre class="crayon-plain-tag">v1beta1storage["flunders/status"] = wardleregistry.RESTInPeace(flunderstorage.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter))</pre>
<p>你甚至可以直接使用父资源的rest.Storage。但是这样的结果是，客户端请求可以仅更新status，也可以更新整个flunder，一般情况下这是不符合预期的。</p>
<p>上面的代码，还会导致在APIServer中注册类似本章开始处的go-restful路由。</p>
<p>抽取路径变量namespace、name的代码是：</p>
<pre class="crayon-plain-tag">pathParams := pathProcessor.ExtractParameters(route, webService, httpRequest.URL.Path)</pre>
<p>这两个变量识别了当前操控的是什么资源。请求进而会转发给rest.Storage的Update方法：</p>
<pre class="crayon-plain-tag">func (e *Store) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {}</pre>
<p>name参数传递的是资源的名字。当前是否应当（仅）更新子资源，rest.Storage无从知晓。</p>
<div class="blog_h2"><span class="graybg">子资源处理器</span></div>
<p>更新状态子资源的时候，我们通常仅仅允许更新Status字段。要达成这个目的，我们需要为子资源注册独立的rest.Storage。</p>
<pre class="crayon-plain-tag">package store

import (
	"context"
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/apiserver/pkg/registry/generic/registry"
	"k8s.io/apiserver/pkg/registry/rest"
	"sigs.k8s.io/apiserver-runtime/pkg/builder/resource/util"
)

// CopyStatusFunc copies status from obj to old
type CopyStatusFunc func(src, dst runtime.Object)

// StatusStore decorates a parent storage and only updates
// status subresource when updating
func StatusStore(parentStore rest.StandardStorage, copyStatusFunc CopyStatusFunc) rest.Storage {
	switch pstor := parentStore.(type) {
	case *registry.Store:
		pstor.UpdateStrategy = &amp;statusStrategy{
			RESTUpdateStrategy: pstor.UpdateStrategy,
			copyStatusFunc:     copyStatusFunc,
		}
	}
	return &amp;statusStore{
		StandardStorage: parentStore,
	}
}

var _ rest.Getter = &amp;statusStore{}
var _ rest.Updater = &amp;statusStore{}

type statusStore struct {
	rest.StandardStorage
}

var _ rest.RESTUpdateStrategy = &amp;statusStrategy{}

// statusStrategy defines a default Strategy for the status subresource.
type statusStrategy struct {
	rest.RESTUpdateStrategy
	copyStatusFunc CopyStatusFunc
}

// PrepareForUpdate calls the PrepareForUpdate function on obj if supported, otherwise does nothing.
func (s *statusStrategy) PrepareForUpdate(ctx context.Context, new, old runtime.Object) {
	s.copyStatusFunc(new, old)
	if err := util.DeepCopy(old, new); err != nil {
		utilruntime.HandleError(err)
	}
}</pre>
<p>genericregistry.Store的更新会在一个原子操作的回调函数中进行。在回调中，它会调用Strategy的PrepareForUpdate方法。上面的statusStore的原理就是覆盖此方法，仅仅改变状态子资源。</p>
<div class="blog_h1"><span class="graybg"><a id="multipleversions"></a>多版本化</span></div>
<p>当你的API需要引入破坏性变更时，就要考虑支持多版本化。</p>
<div class="blog_h2"><span class="graybg">API文件布局</span></div>
<p>下面是一个典型的多版本API文件目录的布局：</p>
<pre class="crayon-plain-tag">api
├── doc.go
├── fullvpcmigration_types.go
├── 
├── v1
│   ├── conversion.go
│   ├── doc.go
│   ├── fullvpcmigration_types.go
│   ├── register.go
│   ├── zz_generated.conversion.go
│   ├── zz_generated.deepcopy.go
│   └── zz_generated.openapi.go
├── v2
│   ├── doc.go
│   ├── fullvpcmigration_types.go
│   ├── register.go
│   ├── zz_generated.conversion.go
│   ├── zz_generated.deepcopy.go
│   └── zz_generated.openapi.go
└── zz_generated.deepcopy.go</pre>
<p> API组的根目录（上面的示例项目只有一个组，因此直接将api目录作为组的根目录）下，应该存放__internal版本的资源结构定义，建议将其内容和最新版本保持一致。</p>
<div class="blog_h3"><span class="graybg">doc.go</span></div>
<p>这个文件应当提供包级别的注释，例如：</p>
<pre class="crayon-plain-tag">// +k8s:openapi-gen=true
// +groupName=gmem.cc
// +kubebuilder:object:generate=true

package api</pre>
<div class="blog_h3"><span class="graybg">register.go</span></div>
<p>这个文件用于Scheme的注册。对于__internal版本：</p>
<pre class="crayon-plain-tag">package api

import (
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

const (
	GroupName = "gmem.cc"
)

var (
	// GroupVersion is group version used to register these objects
	GroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme
	// no &amp;scheme.Builder{} here, otherwise vk __internal/WatchEvent will double registered to k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent &amp;
	// k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent, which is illegal
	SchemeBuilder = runtime.NewSchemeBuilder()

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)

// Kind takes an unqualified kind and returns a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
	return GroupVersion.WithKind(kind).GroupKind()
}

// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
	return GroupVersion.WithResource(resource).GroupResource()
}</pre>
<p>对于普通的版本：</p>
<pre class="crayon-plain-tag">package v2

import (
	"cloud.tencent.com/teleport/api"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

var (
	// GroupVersion is group version used to register these objects
	GroupVersion = schema.GroupVersion{Group: api.GroupName, Version: "v2"}

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme
	SchemeBuilder = runtime.NewSchemeBuilder(func(scheme *runtime.Scheme) error {
		metav1.AddToGroupVersion(scheme, GroupVersion)
		return nil
	})
	localSchemeBuilder = &amp;SchemeBuilder

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)

// Kind takes an unqualified kind and returns a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
	return GroupVersion.WithKind(kind).GroupKind()
}

// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
	return GroupVersion.WithResource(resource).GroupResource()
}</pre>
<p>可以看到，普通版本需要将metav1包中的某些结构注册到自己的GroupVersion。</p>
<div class="blog_h3"><span class="graybg">zz_generated.openapi.go</span></div>
<p>这是每个普通版本都需要生成的OpenAPI定义。这些OpenAPI定义必须注册到API Server，否则将会导致kubectl apply等命令报404错误：</p>
<pre class="crayon-plain-tag">$(OPENAPI_GEN)  \
	--input-dirs "k8s.io/apimachinery/pkg/apis/meta/v1,k8s.io/apimachinery/pkg/runtime,k8s.io/apimachinery/pkg/version" \
	--input-dirs cloud.tencent.com/teleport/api/v1 -o ./  -p api/v1 -O zz_generated.openapi

$(OPENAPI_GEN)  \
	--input-dirs cloud.tencent.com/teleport/api/v2 -o ./  -p api/v2 -O zz_generated.openapi</pre>
<div class="blog_h3"><span class="graybg">zz_generated.conversion.go</span></div>
<p>这是每个普通版本都需要生成的From/To __internal版本的类型转换函数。这些转换函数会通过上面的localSchemeBuilder注册到当前GroupVersion：</p>
<pre class="crayon-plain-tag">$(CONVERSION_GEN) -h hack/boilerplate.go.txt --input-dirs cloud.tencent.com/teleport/api/v1 -O zz_generated.conversion
$(CONVERSION_GEN) -h hack/boilerplate.go.txt --input-dirs cloud.tencent.com/teleport/api/v2 -O zz_generated.conversion</pre>
<p>升级API版本的原因，自然是因为结构出现了变动。结构的变动，就意味着新旧版本有特殊的<span style="background-color: #c0c0c0;">类型转换逻辑。这种逻辑显然不可能自动生成，你手工添加的转换代码应该存放在conversion.go中</span>。</p>
<div class="blog_h3"><span class="graybg">zz_generated.deepcopy.go</span></div>
<p>这个文件是__internal版本、普通版本中的资源对应Go结构都需要生成的深拷贝函数。</p>
<div class="blog_h2"><span class="graybg">关于__internal版本</span></div>
<p>如前文所述，每个API资源（的版本），都需要一个rest.Storage，这个Storage会直接负责该API资源版本的GET/CREATE/UPDATE/DELETE/WATCH等操作。</p>
<p>作为默认的，针对Etcd存储后端的rest.Storage的实现genericregistry.Store，它在内部有一个Cacher。此Cacher利用缓存来处理WATCH/LIST请求，避免对Etcd过频的访问。在此Cacher内部，会使用资源的内部版本。</p>
<p>所谓内部版本，就是注册到__internal这个特殊版本号的资源。<pre class="crayon-plain-tag">__internal</pre>这个字面值由常量<pre class="crayon-plain-tag">runtime.APIVersionInternal</pre>提供。我们通常将<span style="background-color: #c0c0c0;">组的根目录下的资源结构体，注册为__internal版本</span>。</p>
<p>有了这种内部版本机制，Cacher就不需要在内存中，存储资源的不同版本。</p>
<p>除此之外，rest.Storage或者它的Strategy所需要的一系列资源生命周期回调函数，接受的参数，都是__internal版本。这意味着：</p>
<ol>
<li>我们不需要为每个版本，编写重复的回调函数</li>
<li>在多版本化的时候，需要将<span style="background-color: #c0c0c0;">这些回调函数的入参都改为__internal版本</span></li>
</ol>
<div class="blog_h2"><span class="graybg">生成和定制转换函数</span></div>
<p>之所以Cacher、生命周期回调函数，以及下文会提到的，kubectl和存储能够自由的选择自己需要的版本，是因为不同版本的API资源之间可以进行转换。</p>
<p>当你复制一份v1资源的代码为v2时，这时可以使用完全自动生成的转换函数。一旦你添加或修改了一个字段，你就需要定制转换函数了。</p>
<p>假设我们将FullVPCMigrationSpec.TeamName字段改为Team，则需要：</p>
<pre class="crayon-plain-tag">// zz_generated.conversion.go中报错的地方，就是你需要实现的函数

func Convert_v1_FullVPCMigrationSpec_To_api_FullVPCMigrationSpec(in *FullVPCMigrationSpec, out *api.FullVPCMigrationSpec, s conversion.Scope) error {
    // 这里编写因为字段变化还需要手工处理的部分
	out.Team = in.TeamName
    // 然后调用自动生成的函数，这个函数和你实现的函数，名字的差异就是auto前缀
	return autoConvert_v1_FullVPCMigrationSpec_To_api_FullVPCMigrationSpec(in, out, s)
}

func Convert_api_FullVPCMigrationSpec_To_v1_FullVPCMigrationSpec(in *api.FullVPCMigrationSpec, out *FullVPCMigrationSpec, s conversion.Scope) error {
	out.TeamName = in.Team
	return autoConvert_api_FullVPCMigrationSpec_To_v1_FullVPCMigrationSpec(in, out, s)
}</pre>
<p>上面两个，是自动生成的转换代码中，缺失的函数，会导致编译错误。你需要自己实现它们。</p>
<p>带有auto前缀的版本，是自动生成的、完成了绝大部分逻辑的转换函数，你需要进行必要的手工处理，然后调用这个auto函数即可。</p>
<p>需要注意，<span style="background-color: #c0c0c0;">转换函数都是在特定版本和__internal版本之间进行的</span>。也就是如果v1需要转换到v2，则需要先转换为__internal，然后在由__internal转换为v2。这种设计也很好理解，不这样做随着版本的增多，转换函数的数量会爆炸式增长。</p>
<p><span style="background-color: #c0c0c0;">类型转换代码必须注册到Scheme</span>，不管是在API Server、kubectl或controller-runtime这样的客户端，都依赖于Scheme。</p>
<div class="blog_h2"><span class="graybg">多版本如何存储</span></div>
<p>不管是存储（串行化），还是读取（反串行化），都依赖于Codec。所谓Codec就是Serializer：</p>
<pre class="crayon-plain-tag">package runtime

type Serializer interface {
	Encoder
	Decoder
}

// Codec is a Serializer that deals with the details of versioning objects. It offers the same
// interface as Serializer, so this is a marker to consumers that care about the version of the objects
// they receive.
type Codec Serializer</pre>
<p>Codec由CodecFactory提供，后者持有Scheme：</p>
<pre class="crayon-plain-tag">serializer.NewCodecFactory(scheme)</pre>
<p>我们已经知道，Scheme包含这些信息：</p>
<ol>
<li>各种Group、Version、Kind，映射到了什么Go结构</li>
<li>Go结构上具有json标签，这些信息决定了结构的串行化格式是怎样的</li>
<li>同一个Group、Kind的不同Version，如何进行相互转换 </li>
</ol>
<p>因此，Codec有能力进行JSON或其它格式的串行化操作，并且在不同版本的Go结构之间进行转换。</p>
<p>对于 genericregistry.Store 来说，存储就是将API资源的Go结构转换为JSON或者ProtoBuf，保存到Etcd，它显然需要Codec的支持。</p>
<p>当启用多版本支持后，你需要将所有版本（prioritizedVersions）作为参数传递给CodecFactory并创建Codec：</p>
<pre class="crayon-plain-tag">prioritizedVersions ：= []schema.GroupVersion{
    {
        Group: "gmem.cc",
        Version: "v2",
    },
    {
        Group: "gmem.cc",
        Version: "v1",
    },
}
codec := codecFactory.LegacyCodec(prioritizedVersions...)

genericOptions.Etcd.StorageConfig.EncodeVersioner = runtime.NewMultiGroupVersioner(schema.GroupVersion{
    Group: "gmem.cc",
    Version: "v2",
} )</pre>
<p>并且，<span style="background-color: #c0c0c0;">prioritizedVersions决定了存储一个资源的时候，优先选择的格式</span>。例如fullvpcmigrations有v1,v2版本，因此在存储的时候会使用v2。而jointeamrequests只有v1版本，因此存储的时候只能使用v1。</p>
<p>注意：如果存在一个既有的v1版本的fullvpcmigration，<span style="background-color: #c0c0c0;">在上述配置应用后，第一次对它进行修改，会导致存储格式修改为v2</span>。</p>
<div class="blog_h2"><span class="graybg">多版本的OpenAPI</span></div>
<p>你需要为每个版本生成OpenAPI定义。OpenAPI的定义只是一个map，将所有版本的内容合并即可。</p>
<div class="blog_h2"><span class="graybg">APIServer暴露哪个版本</span></div>
<p>APIServer会暴露所有注册的资源版本。但是，它有一个版本优先级的概念：</p>
<pre class="crayon-plain-tag">apiGroupInfo.PrioritizedVersions = prioritizedVersions</pre>
<p>这个决定了kubectl的时候，优先显示为哪个版本。优选版本也会显示在api-resources子命令的输出：</p>
<pre class="crayon-plain-tag"># kubectl -s http://127.0.0.1:6080 api-resources  
NAME                    SHORTNAMES   APIVERSION                 NAMESPACED   KIND
fullvpcmigrations                    gmem.cc/v2   true         FullVPCMigration
jointeamrequests                     gmem.cc/v1   true         JoinTeamRequest</pre>
<p>kuebctl get命令，默认展示优选版本，但是你也可以强制要求显示为指定版本：</p>
<pre class="crayon-plain-tag">kubectl -s http://127.0.0.1:6080 -n default get fullvpcmigration.v1.gmem.cc
# GET http://127.0.0.1:6080/apis/gmem.cc/v1/namespaces/default/fullvpcmigrations?limit=500

kubectl -s http://127.0.0.1:6080 -n default get fullvpcmigration.v2.gmem.cc
# GET http://127.0.0.1:6080/apis/gmem.cc/v2/namespaces/default/fullvpcmigrations?limit=500</pre>
<p>不管怎样，<span style="background-color: #c0c0c0;">存储为任何版本的fullvpcmigrations都会被查询到</span>。你可以认为在<span style="background-color: #c0c0c0;">客户端视角，选择版本仅仅是选择资源的一种“视图”</span>。</p>
<div class="blog_h2"><span class="graybg">控制器中的版本选择</span></div>
<p>控制器所<span style="background-color: #c0c0c0;">监听的资源版本，必须已经在控制器管理器的Scheme中注册</span>。</p>
<p>你在Reconcile代码中，可以<span style="background-color: #c0c0c0;">用任何已经注册的版本来作为Get操作的容器</span>，类型转换会自动进行。  </p>
<p>建议仅在读取、存储资源状态的时候，用普通版本，其余时候，都用__internal版本。这样你的控制器逻辑，在版本升级后，需要的变更会很少。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/kubernetes-style-apiserver">编写Kubernetes风格的APIServer</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/kubernetes-style-apiserver/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>IPVS模式下ClusterIP泄露宿主机端口的问题</title>
		<link>https://blog.gmem.cc/nodeport-leak-under-ipvs-mode</link>
		<comments>https://blog.gmem.cc/nodeport-leak-under-ipvs-mode#comments</comments>
		<pubDate>Tue, 05 Jan 2021 10:50:51 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[IPVS]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=35061</guid>
		<description><![CDATA[<p>问题 在一个启用了IPVS模式kube-proxy的K8S集群中，运行着一个Docker Registry服务。我们尝试通过docker manifest命令（带上--insecure参数）来推送manifest时，出现TLS timeout错误。 这个Registry通过ClusterIP类型的Service暴露访问端点，且仅仅配置了HTTP/80端口。docker manifest命令的--insecure参数的含义是，在Registry不支持HTTPS的情况下，允许使用不安全的HTTP协议通信。从报错上来看，很明显docker manifest认为Registry支持HTTPS协议。 在宿主机上尝试[crayon-69e09a47ac0da894062848-i/]，居然可以连通。检查后发现节点上使用443端口的，只有Ingress Controller的NodePort类型的Service，它在0.0.0.0上监听。删除此NodePort服务后，RegistryClusterIP:443就不通了，docker manifest命令恢复正常。 定义 如果kube-proxy启用了IPVS模式，并且宿主机在0.0.0.0:NonServicePort上监听，那么可以在宿主机上、或者Pod内，通过任意ClusterIP:NonServicePort访问到宿主机的NonServicePort。 这一行为显然不符合预期，我们期望仅仅在Service对象中声明的端口，才可能通过Cluster连通。如果ClusterIP上的未知端口，内核应该丢弃报文或者返回适当的ICMP。 如果kube-proxy使用iptables模式，不会出现这种异常行为。 原因 启用IPVS的情况下，所有ClusterIP都会绑定在kube-ipvs0这个虚拟的网络接口上。例如对于kube-dns服务的ClusterIP 10.96.0.10（ServicePort为TCP 53 / TCP 9153）： [crayon-69e09a47ac0df381843528/] <a class="read-more" href="https://blog.gmem.cc/nodeport-leak-under-ipvs-mode">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/nodeport-leak-under-ipvs-mode">IPVS模式下ClusterIP泄露宿主机端口的问题</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">问题</span></div>
<p>在一个启用了IPVS模式kube-proxy的K8S集群中，运行着一个Docker Registry服务。我们尝试通过docker manifest命令（带上--insecure参数）来推送manifest时，出现TLS timeout错误。</p>
<p>这个Registry通过ClusterIP类型的Service暴露访问端点，且仅仅配置了HTTP/80端口。docker manifest命令的--insecure参数的含义是，在Registry不支持HTTPS的情况下，允许使用不安全的HTTP协议通信。从报错上来看，很明显docker manifest认为Registry支持HTTPS协议。</p>
<p>在宿主机上尝试<pre class="crayon-plain-tag">telnet RegistryClusterIP 443</pre>，居然可以连通。检查后发现节点上使用443端口的，只有Ingress Controller的NodePort类型的Service，它在0.0.0.0上监听。删除此NodePort服务后，RegistryClusterIP:443就不通了，docker manifest命令恢复正常。</p>
<div class="blog_h1"><span class="graybg">定义</span></div>
<p>如果kube-proxy启用了IPVS模式，并且宿主机在0.0.0.0:NonServicePort上监听，那么可以在宿主机上、或者Pod内，通过任意ClusterIP:NonServicePort访问到宿主机的NonServicePort。</p>
<p>这一行为显然不符合预期，我们期望仅仅在Service对象中声明的端口，才可能通过Cluster连通。如果ClusterIP上的未知端口，内核应该丢弃报文或者返回适当的ICMP。</p>
<p>如果kube-proxy使用iptables模式，不会出现这种异常行为。</p>
<div class="blog_h1"><span class="graybg">原因</span></div>
<p>启用IPVS的情况下，所有ClusterIP都会绑定在kube-ipvs0这个虚拟的网络接口上。例如对于kube-dns服务的ClusterIP 10.96.0.10（ServicePort为TCP 53 / TCP 9153）：</p>
<pre class="crayon-plain-tag">5: kube-ipvs0: &lt;BROADCAST,NOARP&gt; mtu 1500 qdisc noop state DOWN group default 
    link/ether fa:d9:9e:37:12:68 brd ff:ff:ff:ff:ff:ff
    inet 10.96.0.10/32 brd 10.96.0.10 scope global kube-ipvs0
       valid_lft forever preferred_lft forever</pre>
<p>这种绑定是必须的，因为IPVS的工作原理是，<span style="background-color: #c0c0c0;">在netfilter挂载点LOCAL_IN上注册钩子ip_vs_in，拦截目的地是VIP（ClusterIP）的封包</span>。而要使得封包进入到LOCAL_IN，它的目的地址必须是本机地址。</p>
<p>每当为网络接口添加一个IP地址，内核都会<span style="background-color: #c0c0c0;">自动</span>在local路由表中增加一条规则，对于上面的10.96.0.10，会增加：</p>
<pre class="crayon-plain-tag"># 对于目的地址是10.96.0.10的封包，从kube-ipvs0发出，如果没有指定源IP，使用10.96.0.10
local 10.96.0.10 dev kube-ipvs0 proto kernel scope host src 10.96.0.10</pre>
<p>上述自动添加路由的一个副作用是，<span style="background-color: #c0c0c0;">对于任意一个端口Port，如果不存在匹配ClusterIP:Port的IPVS规则，同时宿主机上某个应用在0.0.0.0:Port上监听，封包就会交由此应用处理</span>。</p>
<p>在宿主机上执行<pre class="crayon-plain-tag">telnet 10.96.0.10 22</pre>，会发生以下事件序列：</p>
<ol>
<li>出站选路，根据local表路由规则，从kube-ipvs0接口发出封包</li>
<li>由于kube-ipvs0是dummy的，封包<span style="background-color: #c0c0c0;">立刻从kube-ipvs0的出站队列移动到入站队列</span></li>
<li>目的地址是本地地址，因此进入LOCAL_IN挂载点</li>
<li>由于22不是ServicePort，封包被转发给本地进程处理，即监听了22的那个进程</li>
</ol>
<p>如果删除内核自动在local表中添加的路由：</p>
<pre class="crayon-plain-tag">ip route del table local local 10.96.0.10 dev kube-ipvs0 proto kernel scope host src 10.96.0.10</pre>
<p>则会出现以下现象：</p>
<ol>
<li>无法访问10.96.0.10:22。这是我们期望的，因为10.96.0.10这个服务没有暴露22端口，此端口理当不通</li>
<li>无法ping 10.96.0.10。这不是我们期望的，但是一般情况下不会有什么问题。iptables模式下ClusterIP就是无法ping的，IPVS模式下可以在本机ping仅仅是绑定ClusterIP到kube-ipvs0的一个副作用。通常应用程序不应该对ClusterIP做ICMP检测，来判断服务是否可用，因为这依赖了kube-proxy的特定工作模式</li>
<li>在宿主机上，可以访问10.96.0.10:53。这是我们期望的，宿主机上可以访问ClusterIP</li>
<li>在某个容器的网络命名空间下，无法访问10.96.0.10:53。这不是我们期望的，相当于Pod无法访问ClusterIP了</li>
</ol>
<p>以上4条，惟独3难以理解。<span style="background-color: #c0c0c0;">为什么路由没了，宿主机仍然能访问ClusterIP:ServicePort</span>？这个我们还没有从源码级别深究，但是很明显和IPVS有关。IPVS在LOCAL_OUT上挂有钩子，它可能在此钩子中检测到来自本机（主网络命名空间）的、访问ClusterIP+ServicePort（即IPVS虚拟服务）的封包，并进行了某种“魔法”处理，从而避开了没有路由的问题。</p>
<p>下面我们进一步验证上述“魔法”处理的可能性。使用<pre class="crayon-plain-tag">tcpdump -i any host 10.96.0.10</pre>来捕获流量，从容器命名空间访问ClusterIP:ServicePort时，可以看到：</p>
<pre class="crayon-plain-tag">#                  容器IP
11:32:00.448470 IP 172.27.0.24.56378 &gt; 10.96.0.10.53: Flags [S], seq 2946888109, win 28200, options...</pre>
<p>但是从宿主机访问ClusterIP:ServicePort时，则捕获不到任何流量。但是，通过iptables logging，我们可以确定，内核的确<span style="background-color: #c0c0c0;">以ClusterIP为源地址和目的地址</span>，发起了封包：</p>
<pre class="crayon-plain-tag">iptables -t mangle -I OUTPUT 1 -p tcp --dport 53 -j LOG --log-prefix 'out-d53: '

# dmesg -w
#                                      源地址          目的地址
# [3374381.426541] out-d53: IN= OUT=lo SRC=10.96.0.100 DST=10.96.0.100 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=18885 DF PROTO=TCP SPT=42442 DPT=53 WINDOW=86 RES=0x00 ACK URGP=0 </pre>
<p>回顾一下数据报出站、入站的处理过程：</p>
<ol>
<li>出站，依次经过 <strong><span style="background-color: #99cc00;">netfilter/iptables</span></strong> ⇨ <strong><span style="background-color: #cc99ff;">tcpdump</span></strong> ⇨ 网络接口 ⇨网线</li>
<li>入站，依次经过 网线 ⇨ 网络接口 ⇨ tcpdump ⇨ netfilter/iptables</li>
</ol>
<p>只有当IPVS在宿主机请求10.96.0.10的封包出站时，在netfilter中对匹配IPVS虚拟服务的封包进行如下处理，才能解释<span style="background-color: #99cc00;"><strong>iptables</strong></span>中能看到10.96.0.10，而紧随其后的<strong><span style="background-color: #cc99ff;">tcpdump</span></strong>中却又看不到的现象：</p>
<ol>
<li>修改目的地址为Service的Endpoint地址，这就是NAT模式的IPVS（即kube-proxy使用NAT模式）应有的行为</li>
<li>修改了源地址为当前宿主机的地址，不这样做，回程报文就无法路由回来</li>
</ol>
<p>另外注意一下，如果从宿主机访问ClusterIP:NonServicePort，则tcpdump能捕获到源或目的地址为ClusterIP的流量。这是因为IPVS发现它不匹配任何虚拟服务，会直接返回NF_ACCEPT，然后封包就按照常规流程处理了。</p>
<div class="blog_h1"><span class="graybg">后果</span></div>
<div class="blog_h2"><span class="graybg">安全问题</span></div>
<p>如果宿主机上有一个在0.0.0.0上监听的、存在安全漏洞的服务，则可能被恶意的工作负载利用。</p>
<div class="blog_h2"><span class="graybg">行为异常</span></div>
<p>少部分的应用程序，例如docker manifest，其行为取决于端口探测的结果，会无法正常工作。</p>
<div class="blog_h1"><span class="graybg">解决</span></div>
<p>可能的解决方案有：</p>
<ol>
<li>在iptables中匹配哪些针对ClusterIP:NonServicePort的流量，Drop或Reject掉</li>
<li>使用基于fwmark的IPVS虚拟服务，这需要在iptables中对针对ClusterIP:ServicePort的流量打fwmark，而且每个ClusterIP都需要占用独立的fwmark，难以管理</li>
</ol>
<p>对于解决方案1，可以使用如下iptables规则： </p>
<pre class="crayon-plain-tag">#                 如果目的地址是ClusterIP    但是目的端口不是ServicePort           则拒绝
iptables -A INPUT -d  10.96.0.0/12 -m set ! --match-set KUBE-CLUSTER-IP dst,dst -j REJECT</pre>
<p>这个规则能够为容器解决宿主机端口泄露的问题，但是会导致宿主机上无法访问ClusterIP。</p>
<p>引起此问题的原因是，在宿主机访问ClusterIP时，会同时使用ClusterIP作为源地址/目的地址。这样，来自Endpoint的回程报文，unNATed后的目的地址，就会匹配到上面的iptables规则，从而导致封包被Reject掉。</p>
<p>要解决此问题，我们可以修改内核自动添加的路由，提示使用其它地址作为源地址：</p>
<pre class="crayon-plain-tag"># 这条路由给出src提示，当访问10.96.0.10时，选取192.168.104.82（节点IP）作为源地址
ip route replace table local local 10.96.0.10 dev kube-ipvs0 proto kernel scope host src 192.168.104.82</pre>
<div class="blog_h1"><span class="graybg">深入</span></div>
<p>上文我们提到了一个“魔法”处理的猜想，这里我们对IPVS的实现细节进行深入学习，证实此猜想。</p>
<p>本节牵涉到的内核源码均来自linux-3.10.y分支。</p>
<div class="blog_h2"><span class="graybg">Netfilter</span></div>
<p>这是从2.4.x引入内核的一个框架，用于实现防火墙、NAT、封包修改、记录封包日志、用户空间封包排队之类的功能。</p>
<p>netfilter运行在内核中，允许内核模块在Linux网络栈的<span style="background-color: #c0c0c0;">不同位置注册钩子（回调函数），当每个封包穿过网络栈时，这些钩子函数会被调用</span>。</p>
<p><a href="/iptables">iptables</a>是经典的，基于netfilter的用户空间工具。它的继任者是nftables，它更加灵活、可扩容、性能好。</p>
<div class="blog_h3"><span class="graybg">钩子挂载点</span></div>
<p>netfilter提供了5套钩子（的挂载点）：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 180px; text-align: center;">挂载点</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>NF_IP_PER_ROUTING</td>
<td>
<p>当封包进入网络栈时调用。封包的目的地可能是本机，或者需要转发</p>
<p>ip_rcv / ipv6_rcv是内核接受并处理IP数据报的入口，此函数会调用这类钩子：</p>
<pre class="crayon-plain-tag">int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
	// ...
	return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
		       ip_rcv_finish);
}</pre>
</td>
</tr>
<tr>
<td>NF_IP_LOCAL_IN</td>
<td>
<p>当路由判断封包应该由本机处理时（目的地址是本机地址）调用
<p>ip_local_deliver / ip6_input负责将IP数据报向上层传递，此函数会调用这类钩子</p>
</td>
</tr>
<tr>
<td>NF_IP_FORWARD</td>
<td>
<p>当路由判断封包应该被转发给其它机器（或者网络命名空间）时调用</p>
<p>ip_forward / ip6_forward负责封包转发，此函数会调用这类钩子</p>
</td>
</tr>
<tr>
<td>NF_IP_POST_ROUTING</td>
<td>
<p>在封包即将离开网络栈（进入网线）时调用，不管是转发的、还是本机发出的，都需要经过此挂载点</p>
<p>ip_output / ip6_finish_output2会调用这类钩子</p>
</td>
</tr>
<tr>
<td>NF_IP_LOCAL_OUT</td>
<td>
<p>当封包由本机产生，需要往外发送时调用</p>
<p>__ip_local_out / __ip6_local_out会调用这类钩子</p>
</td>
</tr>
</tbody>
</table>
<p>这些挂载点，和iptables的各链是对应的。</p>
<div class="blog_h3"><span class="graybg">注册钩子</span></div>
<p>要在内核中使用netfilter的钩子，你需要调用函数：</p>
<pre class="crayon-plain-tag">// 注册钩子
int nf_register_hook(struct nf_hook_ops *reg){}
// 反注册钩子
void nf_unregister_hook(struct nf_hook_ops *reg){}</pre>
<p>入参nf_hook_ops是一个结构：</p>
<pre class="crayon-plain-tag">struct nf_hook_ops {
	// 钩子的函数指针，依据内核的版本不同此函数的签名有所差异
	nf_hookfn		*hook;
	struct net_device	*dev;
	void			*priv;
	// 钩子针对的协议族，PF_INET表示IPv4
	u_int8_t		pf;
	// 钩子类型代码，参考上面的表格
	unsigned int		hooknum;
	// 每种类型的钩子，都可以有多个，此数字决定执行优先级
	int			priority;
};


// 钩子函数的签名
typedef unsigned int nf_hookfn(unsigned int hooknum,
			       struct sk_buff *skb, // 正被处理的数据报
			       const struct net_device *in, // 输入设备
			       const struct net_device *out, // 是出设备
			       int (*okfn)(struct sk_buff *)); // 如果通过钩子检查，则调用此函数，通常用不到</pre>
<div class="blog_h3"><span class="graybg">钩子返回值</span></div>
<pre class="crayon-plain-tag">/* Responses from hook functions. */
// 丢弃该报文，不再继续传输或处理
#define NF_DROP 0
// 继续正常传输报文，如果后面由低优先级的钩子，仍然会调用它们
#define NF_ACCEPT 1
// 告知netfilter，报文被别人偷走处理了，不需要再对它做任何处理
// 下文的分析中，我们有个例子。一个netfilter钩子在内部触发了对netfilter钩子的调用
// 外层钩子返回的就是NF_STOLEN，相当于将封包的控制器转交给内层钩子了
#define NF_STOLEN 2
// 对该数据报进行排队，通常用于将数据报提交给用户空间进程处理
#define NF_QUEUE 3
// 再次调用该钩子函数
#define NF_REPEAT 4
// 继续正常传输报文，不会调用此挂载点的后续钩子
#define NF_STOP 5
#define NF_MAX_VERDICT NF_STOP </pre>
<div class="blog_h3"><span class="graybg">钩子优先级</span></div>
<p>优先级通常以下面的枚举为基准+/-：</p>
<pre class="crayon-plain-tag">enum nf_ip_hook_priorities {
	// 数值越小，优先级越高，越先执行
	NF_IP_PRI_FIRST = INT_MIN,
	NF_IP_PRI_CONNTRACK_DEFRAG = -400,
	// 可以看到iptables各表注册的钩子的优先级
	NF_IP_PRI_RAW = -300,
	NF_IP_PRI_SELINUX_FIRST = -225,
	NF_IP_PRI_CONNTRACK = -200,
	NF_IP_PRI_MANGLE = -150,
	NF_IP_PRI_NAT_DST = -100,
	NF_IP_PRI_FILTER = 0,
	NF_IP_PRI_SECURITY = 50,
	NF_IP_PRI_NAT_SRC = 100,
	NF_IP_PRI_SELINUX_LAST = 225,
	NF_IP_PRI_CONNTRACK_HELPER = 300,
	NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
	NF_IP_PRI_LAST = INT_MAX,
};</pre>
<div class="blog_h2"><span class="graybg"><a id="ipvs"></a>IPVS</span></div>
<div class="blog_h3"><span class="graybg">钩子列表</span></div>
<p> ip_vs模块初始化时，会通过ip_vs_init函数，调用nf_register_hook，注册以下netfilter钩子：</p>
<pre class="crayon-plain-tag">static struct nf_hook_ops ip_vs_ops[] __read_mostly = {
	// 注册到LOCAL_IN，这两个钩子处理外部客户端的报文
	// 转而调用ip_vs_out，用于NAT模式下，处理LVS回复外部客户端的报文，例如修改IP地址
	{
		.hook		= ip_vs_reply4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_LOCAL_IN,
		.priority	= NF_IP_PRI_NAT_SRC - 2,
	},
	// 转而调用ip_vs_in，用于处理外部客户端进入IPVS的请求报文
	// 如果没有对应请求报文的连接，则使用调度函数创建连接结构，这其中牵涉选择RS负载均衡算法
	{
		.hook		= ip_vs_remote_request4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_LOCAL_IN,
		.priority	= NF_IP_PRI_NAT_SRC - 1,
	},

	// 注册到LOCAL_OUT，这两个钩子处理LVS本机的报文
	// 转而调用ip_vs_out，用于NAT模式下，处理LVS回复客户端的报文
	{
		.hook		= ip_vs_local_reply4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_LOCAL_OUT,
		.priority	= NF_IP_PRI_NAT_DST + 1,
	},
	// 转而调用ip_vs_in，调度并转发（给RS）本机的请求
	{
		.hook		= ip_vs_local_request4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_LOCAL_OUT,
		.priority	= NF_IP_PRI_NAT_DST + 2,
	},

	// 这两个函数注册到FORWARD
	// 转而调用ip_vs_in_icmp，用于处理外部客户端发到IPVS的ICMP报文，并转发到RS
	{
		.hook		= ip_vs_forward_icmp,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_FORWARD,
		.priority	= 99,
	},
	// 转而调用ip_vs_out，用于NAT模式下，修改RS给的应答报文的源地址为IPVS虚拟地址
	{
		.hook		= ip_vs_reply4,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_FORWARD,
		.priority	= 100,
	}
};</pre>
<div class="blog_h3"><span class="graybg">ip_vs_in</span></div>
<p>从上面的钩子我们可以看到：</p>
<ol>
<li>针对外部发起的、本机发起的，对IPVS的请求（目的是VIP的SYN），钩子的位置是不一样的：
<ol>
<li>对于外部的请求，在LOCAL_IN中处理，钩子函数为ip_vs_remote_request4</li>
<li>对于本机的请求，在LOCAL_OUT中处理，钩子函数为ip_vs_local_request4</li>
</ol>
</li>
<li> 尽管钩子的位置不同，但是函数ip_vs_remote_request4、ip_vs_local_request4都是调用ip_vs_in。实际上，这两个函数的逻辑完全一样：<br />
<pre class="crayon-plain-tag">/*
 *	AF_INET handler in NF_INET_LOCAL_IN chain
 *	Schedule and forward packets from remote clients
 */
static unsigned int
ip_vs_remote_request4(unsigned int hooknum, struct sk_buff *skb,
		      const struct net_device *in,
		      const struct net_device *out,
		      int (*okfn)(struct sk_buff *))
{
	return ip_vs_in(hooknum, skb, AF_INET);
}

/*
 *	AF_INET handler in NF_INET_LOCAL_OUT chain
 *	Schedule and forward packets from local clients
 */
static unsigned int
ip_vs_local_request4(unsigned int hooknum, struct sk_buff *skb,
		     const struct net_device *in, const struct net_device *out,
		     int (*okfn)(struct sk_buff *))
{
	return ip_vs_in(hooknum, skb, AF_INET);
}</pre>
</li>
</ol>
<p>回顾一下上文我们关于“魔法”处理的疑惑。对于从宿主机发起对10.96.0.10:53的请求，我们通过iptables logging证实了使用的源IP地址是10.96.0.10：</p>
<ol>
<li>这个请求为什么tcpdump捕获不到？</li>
<li>为什么删除路由不影响宿主机对ClusterIP的请求（却又导致容器无法请求ClusterIP）？</li>
</ol>
<p>这两个问题的答案，很可能就隐藏在ip_vs_in函数中，因为它是处理进入IPVS的数据报的统一入口。如果该函数同时修改了原始封包的源/目的地址，就解释了问题1；如果该函数在内部进行了选路操作，则解释了问题2。</p>
<p>下面分析一下ip_vs_in的代码：</p>
<pre class="crayon-plain-tag">static unsigned int
ip_vs_in(unsigned int hooknum, struct sk_buff *skb, int af)
{
	// 网络命名空间
	struct net *net;
	// IPVS的IP头，其中存有3层头len、协议、标记、源/目的地址
	struct ip_vs_iphdr iph;
	// 持有协议（TCP/UDP/SCTP/AH/ESP）信息，更重要的是带着很多函数指针。这些指针负责针对特定协议的IPVS逻辑
	struct ip_vs_protocol *pp;
	// 每个命名空间一个此对象，包含统计计数器、超时表
	struct ip_vs_proto_data *pd;
	// 当前封包所属的IPVS连接对象，此对象最重要的是packet_xmit函数指针。它负责将封包发走
	struct ip_vs_conn *cp;
	int ret, pkts;
	// 描述当前网络命名空间的IPVS状态
	struct netns_ipvs *ipvs;

	// 如果封包已经被标记为IPVS请求/应答，不做处理，继续netfilter常规流程
	// 后续ip_vs_nat_xmit会让封包“重入”netfilter，那时封包已经打上IPVS标记
	// 这里的判断确保重入的封包走netfilter常规流程，而不是进入死循环
	if (skb-&gt;ipvs_property)
		return NF_ACCEPT;


	// 如果封包目的地不是本机且当前不在LOCAL_OUT
	// 或者封包的dst_entry不存在，不做处理，继续netfilter常规流程
	if (unlikely((skb-&gt;pkt_type != PACKET_HOST &amp;&amp;
		      hooknum != NF_INET_LOCAL_OUT) ||
		     !skb_dst(skb))) {
		ip_vs_fill_iph_skb(af, skb, &amp;iph);
		IP_VS_DBG_BUF(12, "packet type=%d proto=%d daddr=%s"
			      " ignored in hook %u\n",
			      skb-&gt;pkt_type, iph.protocol,
			      IP_VS_DBG_ADDR(af, &amp;iph.daddr), hooknum);
		return NF_ACCEPT;
	}
	// 如果当前IPVS主机是backup，或者当前命名空间没有启用IPVS，不做处理，继续netfilter常规流程
	net = skb_net(skb);
	ipvs = net_ipvs(net);
	if (unlikely(sysctl_backup_only(ipvs) || !ipvs-&gt;enable))
		return NF_ACCEPT;

	// 使用封包的IP头填充IPVS的IP头
	ip_vs_fill_iph_skb(af, skb, &amp;iph);

	// 如果是RAW套接字，不做处理，继续netfilter常规流程
	if (unlikely(skb-&gt;sk != NULL &amp;&amp; hooknum == NF_INET_LOCAL_OUT &amp;&amp;
		     af == AF_INET)) {
		struct sock *sk = skb-&gt;sk;
		struct inet_sock *inet = inet_sk(skb-&gt;sk);

		if (inet &amp;&amp; sk-&gt;sk_family == PF_INET &amp;&amp; inet-&gt;nodefrag)
			return NF_ACCEPT;
	}

	// 处理ICMP报文，和我们的场景无关
	if (unlikely(iph.protocol == IPPROTO_ICMP)) {
		int related;
		int verdict = ip_vs_in_icmp(skb, &amp;related, hooknum);
		if (related)
			return verdict;
	}

	// 如果协议不受IPVS支持，不做处理，继续netfilter常规流程
	pd = ip_vs_proto_data_get(net, iph.protocol);
	if (unlikely(!pd))
		return NF_ACCEPT;
	// 协议被支持，得到pp
	pp = pd-&gt;pp;
	// 尝试获取封包所属的IPVS连接对象
	cp = pp-&gt;conn_in_get(af, skb, &amp;iph, 0);
	// 如果封包属于既有IPVS连接，且此连接的RS（dest）已经设置，且RS的权重为0
	// 认为是无效连接，设为过期
	if (unlikely(sysctl_expire_nodest_conn(ipvs)) &amp;&amp; cp &amp;&amp; cp-&gt;dest &amp;&amp;
	    unlikely(!atomic_read(&amp;cp-&gt;dest-&gt;weight)) &amp;&amp; !iph.fragoffs &amp;&amp;
	    is_new_conn(skb, &amp;iph)) {
		ip_vs_conn_expire_now(cp);
		__ip_vs_conn_put(cp);
		cp = NULL;
	}

	// 调度一个新的IPVS连接，这里牵涉到RS的LB算法
	if (unlikely(!cp) &amp;&amp; !iph.fragoffs) {
		int v;
		if (!pp-&gt;conn_schedule(af, skb, pd, &amp;v, &amp;cp, &amp;iph))
			// 如果返回0，通常v是NF_DROP，这以为这调度失败，封包丢弃
			return v;
	}

	if (unlikely(!cp)) {
		IP_VS_DBG_PKT(12, af, pp, skb, 0,
			      "ip_vs_in: packet continues traversal as normal");
		if (iph.fragoffs) {
			IP_VS_DBG_RL("Unhandled frag, load nf_defrag_ipv6\n");
			IP_VS_DBG_PKT(7, af, pp, skb, 0, "unhandled fragment");
		}
		return NF_ACCEPT;
	}

	// 入站封包 —— 在我们的场景中，这是本地客户端入了IPVS系统的封包
	// 从网络栈的角度来说，我们正在处理的是出站封包...
	IP_VS_DBG_PKT(11, af, pp, skb, 0, "Incoming packet");

	// IPVS连接的RS不可用
	if (cp-&gt;dest &amp;&amp; !(cp-&gt;dest-&gt;flags &amp; IP_VS_DEST_F_AVAILABLE)) {
		// 立即将连接设为过期
		if (sysctl_expire_nodest_conn(ipvs)) {
			ip_vs_conn_expire_now(cp);
		}
		// 丢弃封包
		__ip_vs_conn_put(cp);
		return NF_DROP;
	}
	// 更新计数器
	ip_vs_in_stats(cp, skb);
	// 更新IPVS连接状态机，做的事情包括
	//   根据数据包 tcp 标记字段来更新当前状态机
	//   更新连接对应的统计数据，包括：活跃连接和非活跃连接
	//   根据连接状态，设置超时时间
	ip_vs_set_state(cp, IP_VS_DIR_INPUT, skb, pd);

	if (cp-&gt;packet_xmit)
		// 调用packet_xmit将封包发走，实际上是重入netfilter的LOCAL_OUT，封包控制权转移走，后续不该再操控skb
		ret = cp-&gt;packet_xmit(skb, cp, pp, &amp;iph);
	else {
		IP_VS_DBG_RL("warning: packet_xmit is null");
		ret = NF_ACCEPT;
	}

	if (cp-&gt;flags &amp; IP_VS_CONN_F_ONE_PACKET)
		pkts = sysctl_sync_threshold(ipvs);
	else
		pkts = atomic_add_return(1, &amp;cp-&gt;in_pkts);

	if (ipvs-&gt;sync_state &amp; IP_VS_STATE_MASTER)
		ip_vs_sync_conn(net, cp, pkts);

	// 放回连接对象，重置连接定时器
	ip_vs_conn_put(cp);
	return ret;
}</pre>
<p>上面这段代码中，“魔法”处理最可能发生在：</p>
<ol>
<li>conn_schedule：在这里需要进行IPVS连接的调度</li>
<li>packet_xmit：在这里发送经过IPVS处理的封包</li>
</ol>
<p>二者都是函数指针，在TCP协议下，conn_schedule指向tcp_conn_schedule。在NAT模式下，packet_xmit指向ip_vs_nat_xmit。packet_xmit指针是在conn_schedule过程中初始化的。</p>
<div class="blog_h3"><span class="graybg">tcp_conn_schedule</span></div>
<p>我们看一下TCP协议下IPVS连接的调度过程。</p>
<pre class="crayon-plain-tag">static int
tcp_conn_schedule(int af, struct sk_buff *skb, struct ip_vs_proto_data *pd,
		  int *verdict, struct ip_vs_conn **cpp,
		  struct ip_vs_iphdr *iph)
{
	// 网络命名空间
	struct net *net;
	// IPVS虚拟服务对象
	struct ip_vs_service *svc;
	struct tcphdr _tcph, *th;

	// 解析L4头，如果失败，提示ip_vs_in丢弃封包
	th = skb_header_pointer(skb, iph-&gt;len, sizeof(_tcph), &amp;_tcph);
	if (th == NULL) {
		*verdict = NF_DROP;
		return 0;
	}
	net = skb_net(skb);
	rcu_read_lock();
	if (th-&gt;syn &amp;&amp;
	    // 根据封包特征，去查找匹配的虚拟服务
	    (svc = ip_vs_service_find(net, af, skb-&gt;mark, iph-&gt;protocol,
				      &amp;iph-&gt;daddr, th-&gt;dest))) {
		int ignored;
		// 如果当前网络命名空间“过载”了，丢弃封包
		if (ip_vs_todrop(net_ipvs(net))) {
			rcu_read_unlock();
			*verdict = NF_DROP;
			return 0;
		}

		// 选择一个RS，建立IPVS连接
		// 如果找不到RS，或者发生致命错误，则ignore为0或-1，这种情况下
		// IPVS连接没有成功创建，提示ip_vs_in丢弃封包，可能附带回复ICMP
		*cpp = ip_vs_schedule(svc, skb, pd, &amp;ignored, iph);
		if (!*cpp &amp;&amp; ignored &lt;= 0) {
			if (!ignored)
				// ignored=0，找不到RS
				*verdict = ip_vs_leave(svc, skb, pd, iph);
			else
				*verdict = NF_DROP;
			rcu_read_unlock();
			return 0;
		}
	}
	rcu_read_unlock();
	// 如果调度成功，IPVS连接对象不为空，返回1
	/* NF_ACCEPT */
	return 1;
}</pre>
<p>到这里我们还没有看到IPVS对封包地址进行更改，需要进一步阅读ip_vs_schedule。 </p>
<div class="blog_h3"><span class="graybg">ip_vs_schedule</span></div>
<p>这是IPVS调度的核心函数，它支持TCP/UDP，它为虚拟服务选择一个RS，创建IPVS连接对象。</p>
<pre class="crayon-plain-tag">struct ip_vs_conn *
ip_vs_schedule(struct ip_vs_service *svc, struct sk_buff *skb,
	       struct ip_vs_proto_data *pd, int *ignored,
	       struct ip_vs_iphdr *iph)
{
	struct ip_vs_protocol *pp = pd-&gt;pp;
	// IPVS连接对象（connection entry）
	struct ip_vs_conn *cp = NULL;
	struct ip_vs_scheduler *sched;
	struct ip_vs_dest *dest;
	__be16 _ports[2], *pptr;
	unsigned int flags;

	// ...

	*ignored = 0;

	/*
	 *    Non-persistent service
	 */
	// 调度工作委托给虚拟服务的scheduler
	sched = rcu_dereference(svc-&gt;scheduler);
	// 调度器就是选择一个RS（ip_vs_dest）
	dest = sched-&gt;schedule(svc, skb);
	if (dest == NULL) {
		IP_VS_DBG(1, "Schedule: no dest found.\n");
		return NULL;
	}

	flags = (svc-&gt;flags &amp; IP_VS_SVC_F_ONEPACKET
		 &amp;&amp; iph-&gt;protocol == IPPROTO_UDP) ?
		IP_VS_CONN_F_ONE_PACKET : 0;

	// 初始化IPVS连接对象 ip_vs_conn
	{
		struct ip_vs_conn_param p;

		ip_vs_conn_fill_param(svc-&gt;net, svc-&gt;af, iph-&gt;protocol,
				      &amp;iph-&gt;saddr, pptr[0], &amp;iph-&gt;daddr,
				      pptr[1], &amp;p);
		// 操控ip_vs_conn的逻辑包括：
		//   初始化定时器
		//   设置网络命名空间
		//   设置地址、fwmark、端口
		//   根据IP版本、IPVS模式（NAT/DR/TUN）为连接设置一个packet_xmit
		cp = ip_vs_conn_new(&amp;p, &amp;dest-&gt;addr,
				    dest-&gt;port ? dest-&gt;port : pptr[1],
				    flags, dest, skb-&gt;mark);
		if (!cp) {
			*ignored = -1;
			return NULL;
		}
	}

	// ...
	return cp;
}</pre>
<p>到这里我们可以看到， conn_schedule仍然没有对封包做任何修改。看来关键在packet_xmit函数中。</p>
<div class="blog_h3"><span class="graybg">ip_vs_nat_xmit</span></div>
<pre class="crayon-plain-tag">int ip_vs_nat_xmit(struct sk_buff *skb, struct ip_vs_conn *cp,
	       struct ip_vs_protocol *pp, struct ip_vs_iphdr *ipvsh)
{
	// 路由表项
	struct rtable *rt;		/* Route to the other host */
	// 是否本机    是否输入路由
	int local, rc, was_input;

	EnterFunction(10);

	rcu_read_lock();
	// 是否尚未设置客户端端口
	if (unlikely(cp-&gt;flags &amp; IP_VS_CONN_F_NO_CPORT)) {
		__be16 _pt, *p;

		p = skb_header_pointer(skb, ipvsh-&gt;len, sizeof(_pt), &amp;_pt);
		if (p == NULL)
			goto tx_error;
		// 设置IPVS连接对象的cport
		// caddr cport 客户端地址
		// vaddr vport 虚拟服务地址
		// daddr dport RS地址
		ip_vs_conn_fill_cport(cp, *p);
		IP_VS_DBG(10, "filled cport=%d\n", ntohs(*p));
	}

	was_input = rt_is_input_route(skb_rtable(skb));
	// 出口路由查找，依据是封包、RS的地址、以及若干标识位
	// 返回值提示路由目的地是否是本机
	local = __ip_vs_get_out_rt(skb, cp-&gt;dest, cp-&gt;daddr.ip,
				   IP_VS_RT_MODE_LOCAL |
				   IP_VS_RT_MODE_NON_LOCAL |
				   IP_VS_RT_MODE_RDR, NULL);
	if (local &lt; 0)
		goto tx_error;
	rt = skb_rtable(skb);

	// 如果目的地是本机，RS地址是环回地址，是输入
	if (local &amp;&amp; ipv4_is_loopback(cp-&gt;daddr.ip) &amp;&amp; was_input) {
		IP_VS_DBG_RL_PKT(1, AF_INET, pp, skb, 0, "ip_vs_nat_xmit(): "
				 "stopping DNAT to loopback address");
		goto tx_error;
	}

	// 封包将被修改，执行copy-on-write
	if (!skb_make_writable(skb, sizeof(struct iphdr)))
		goto tx_error;

	if (skb_cow(skb, rt-&gt;dst.dev-&gt;hard_header_len))
		goto tx_error;

	// 修改封包，dnat_handler指向tcp_dnat_handler
	if (pp-&gt;dnat_handler &amp;&amp; !pp-&gt;dnat_handler(skb, pp, cp, ipvsh))
		goto tx_error;
	// 更改目的地址
	ip_hdr(skb)-&gt;daddr = cp-&gt;daddr.ip;
	// 为出站封包生成chksum
	ip_send_check(ip_hdr(skb));

	IP_VS_DBG_PKT(10, AF_INET, pp, skb, 0, "After DNAT");

	skb-&gt;local_df = 1;

	// 发送封包：
	//   如果发送出去了，返回 NF_STOLEN
	//   如果没有发送（local=1，目的地是本机），返回NF_ACCEPT
	rc = ip_vs_nat_send_or_cont(NFPROTO_IPV4, skb, cp, local);
	rcu_read_unlock();

	LeaveFunction(10);
	return rc;

  tx_error:
	kfree_skb(skb);
	rcu_read_unlock();
	LeaveFunction(10);
	return NF_STOLEN;
}

static inline int ip_vs_nat_send_or_cont(int pf, struct sk_buff *skb,  struct ip_vs_conn *cp, int local)
{
	// 注意这个NF_STOLEN的含义，参考上文
	int ret = NF_STOLEN;
	// 给封包设置IPVS标记，NF_HOOK会导致当前封包重入netfilter，此标记会让重入后的封包立即NF_ACCEPT、
	// 重入让修改后的封包有机会被ipables处理
	skb-&gt;ipvs_property = 1;
	if (likely(!(cp-&gt;flags &amp; IP_VS_CONN_F_NFCT)))
		ip_vs_notrack(skb);
	else
		ip_vs_update_conntrack(skb, cp, 1);
	// 如果目的地不是本机
	if (!local) {
		skb_forward_csum(skb);
		// 调用LOCAL_OUT挂载点
		NF_HOOK(pf, NF_INET_LOCAL_OUT, skb, NULL, skb_dst(skb)-&gt;dev, dst_output);
	} else
		ret = NF_ACCEPT;
	return ret;
}</pre>
<p>在ip_vs_nat_xmit中，我们可以了解到，对于宿主机发起的针对ClusterIP:ServicePort的请求</p>
<ol>
<li>封包的目的地址被修改为Endpoint（通常是Pod，IPVS中的RS）的地址</li>
<li>修改后的封包，重新被塞入netfilter（内层），注意当前就正在netfilter（外层）中
<ol>
<li>外层钩子的返回值是NF_STOLEN：封包处理权转移给内层钩子，停止后续netfilter流程</li>
<li>内层钩子的返回值是NF_ACCEPT：不做IPVS相关处理，继续后续netfilter流程。IPVS前、后的LOCAL_OUT、POSTROUTING钩子都会正常执行。也就是说，对于修改后的封包，内核会进行完整、常规的netfilter处理，就像没有IPVS存在一样</li>
</ol>
</li>
</ol>
<p>到这里，我们确定了，IPVS会在LOCAL_OUT中进行DNAT。但是只有同时进行SNAT，才能解释上文的中的疑惑。</p>
<div class="blog_h2"><span class="graybg">SNAT</span></div>
<p>花费了不少时间在IPVS上探究后，我们意识到走错了方向。我们忘记了SANT是kube-proxy会去做的事情。查看一下iptables规则就一目了然了：</p>
<pre class="crayon-plain-tag"># iptables -t nat -L -n -v

Chain OUTPUT (policy ACCEPT 2 packets, 150 bytes)
 pkts bytes target     prot opt in     out     source        destination         
# 所有出站流量都要经过自定义的 KUBE-SERVICES 链
  21M 3825M KUBE-SERVICES  all  --  *  *   0.0.0.0/0         0.0.0.0/0            /* kubernetes service portals */

Chain KUBE-SERVICES (2 references)
 pkts bytes target     prot opt in     out     source        destination         
# 如果目的IP:PORT属于K8S服务，则调用KUBE-MARK-MASQ链
    0     0 KUBE-MARK-MASQ  all  --  * *  !172.27.0.0/16     0.0.0.0/0   match-set KUBE-CLUSTER-IP dst,dst

# 给封包打上标记 0x4000
Chain KUBE-MARK-MASQ (5 references)
 pkts bytes target     prot opt in     out     source         destination         
   98  5880 MARK       all  --  *       *  0.0.0.0/0          0.0.0.0/0            MARK or 0x4000

 
Chain POSTROUTING (policy ACCEPT 2 packets, 150 bytes)
 pkts bytes target     prot opt in     out     source         destination         
  44M 5256M KUBE-POSTROUTING  all  --  *  *       0.0.0.0/0   0.0.0.0/0            /* kubernetes postrouting rules */

Chain KUBE-POSTROUTING (1 references)
 pkts bytes target     prot opt in     out     source         destination         
# 仅仅处理 0x4000标记的封包
 1781  166K RETURN     all  --  *      *  0.0.0.0/0           0.0.0.0/0            mark match ! 0x4000/0x4000
# 执行SNAT     
   97  5820 MARK       all  --  *      *  0.0.0.0/0           0.0.0.0/0   MARK xor 0x4000
   97  5820 MASQUERADE  all  --  *     *  0.0.0.0/0           0.0.0.0/0   /* kubernetes service traffic requiring SNAT */</pre>
<p>由于ip_vs_local_request4挂钩在LOCAL_OUT，优先级为NF_IP_PRI_NAT_DST+2 ，因此它是发生在上面nat表OUTPUT链中MARK之后的。也就是说在IPVS处理之前，kube-proxy已经给原始的封包打上标记。</p>
<p>重入的、DNAT后的封包进入LOCAL_OUT，随后进入POSTROUTING。由于标记的缘故，封包被kube-proxy的规则SNAT。</p>
<p>经过POSTROUTING的封包，经过tcpdump，但是由于源、目的IP地址，以及目的端口都改变了，因而我们看到tcpdump没有任何输出。</p>
<div class="blog_h1"><span class="graybg">总结</span></div>
<p>这里做一下小结。</p>
<p>为什么IPVS模式下，能够ping通ClusterIP？ 这是因为IPVS模式下，ClusterIP被配置为宿主机上一张虚拟网卡kube-ipvs0的IP地址。</p>
<p>为什么IPVS模式下，宿主机端口被ClusterIP泄漏？每当添加一个ClusterIP给网络接口后，内核自动在local表中增加一条路由，此路由保证了针对ClusterIP的访问，在没有IPVS干涉的情况下，路由到本机处理。这样，在0.0.0.0上监听的进程，就接收到报文并处理。</p>
<p>为什么删除内核添加的路由后：</p>
<ol>
<li>宿主机上访问ClusterIP:NonServicePort不通了？因为没有路由了</li>
<li>没有路由了，为什么宿主机上访问ClusterIP:ServicePort仍然畅通？如上文分析，IPVS在ip_vs_nat_xmit中仍然会进行选路操作</li>
<li>那为什么从容器网络命名空间访问ClusterIP:ServicePort不通呢？IPVS处理本地、远程客户端的代码路径不一样。容器网络命名空间是远程客户端，需要首先进入PER_ROUTING，然后选路，路由目的地是本机，才会进入LOCAL_IN，IPVS才有介入的时机。由于路由被删掉了，选路那一步就会出问题</li>
</ol>
<p>为什么通过--match-set KUBE-CLUSTER-IP匹配目的地址，如果封包目的端口是NonServicePort则Reject：</p>
<ol>
<li>这种方案对容器命名空间有效？容器请求的源地址不会是ClusterIP，因此回程报文的目的地址不会因为匹配规则而Reject</li>
<li>这种方案导致宿主机无法访问ClusterIP？宿主机发起请求时用的是ClusterIP，请求端口是随机的。这种请求的回程报文必然匹配规则导致Reject</li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/nodeport-leak-under-ipvs-mode">IPVS模式下ClusterIP泄露宿主机端口的问题</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/nodeport-leak-under-ipvs-mode/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Galaxy学习笔记</title>
		<link>https://blog.gmem.cc/galaxy-study-note</link>
		<comments>https://blog.gmem.cc/galaxy-study-note#comments</comments>
		<pubDate>Sat, 01 Aug 2020 11:28:21 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[CNI]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=36521</guid>
		<description><![CDATA[<p>简介 Galaxy是TKEStack的一个网络组件，支持为TKE集群提供Overlay/Underlay容器网络。Galaxy的一个特性是能够提供浮动IP（弹性IP） —— 即使Pod因为节点宕机而漂移到其它节点，其IP地址也能够保持不变。 网络类型 Galaxy提供四类网络，支持为每个工作负载单独配置网络模式。 Overlay网络 默认模式，基于Flannel的VXLAN或者Host Gateway（路由）方案。同节点容器通信不走网桥，报文直接利用主机路由转发，跨节点容器通信利用VXLAN协议封装或者直接路由，节点间路由记录在Etcd中。优点：简单可靠，性能不错，支持网络策略。 tke-installer默认安装的TKEStack会自动配置Galaxy为Overlay模式，CNI配置： [crayon-69e09a47ace03308619468/] 在该模式下： Flannel在每个Kubelet节点上分配一个子网，并将其保存在etcd和本地路径[crayon-69e09a47ace07840801385-i/] Kubelet调用Galaxy的CNI插件galaxy-sdn，该插件会通过UDS调用本机的Galaxy进程 Galaxy进程则又调用Flannel CNI，解析/run/flannel/subnet.env中的子网信息 Flannel CNI会调用： Bridge CNI或Veth CNI来为POD配置网络 调用host-lo <a class="read-more" href="https://blog.gmem.cc/galaxy-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/galaxy-study-note">Galaxy学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Galaxy是TKEStack的一个网络组件，支持为TKE集群提供Overlay/Underlay容器网络。Galaxy的一个特性是能够提供浮动IP（弹性IP） —— 即使Pod因为节点宕机而漂移到其它节点，其IP地址也能够保持不变。</p>
<div class="blog_h2"><span class="graybg">网络类型</span></div>
<p>Galaxy提供四类网络，支持为每个工作负载单独配置网络模式。</p>
<div class="blog_h3"><span class="graybg">Overlay网络</span></div>
<p>默认模式，基于Flannel的VXLAN或者Host Gateway（路由）方案。<span style="background-color: #c0c0c0;">同节点容器通信不走网桥，报文直接利用主机路由转发</span>，<span style="background-color: #c0c0c0;">跨节点容器通信利用VXLAN协议封装或者直接路由</span>，<span style="background-color: #c0c0c0;">节点间路由记录在Etcd</span>中。优点：简单可靠，性能不错，支持网络策略。</p>
<p>tke-installer默认安装的TKEStack会自动配置Galaxy为Overlay模式，CNI配置：</p>
<pre class="crayon-plain-tag">{
  "type": "galaxy-sdn",
  "capabilities": {"portMappings": true},
  "cniVersion": "0.2.0"
} </pre>
<p>在该模式下：</p>
<ol>
<li>Flannel在每个Kubelet节点上分配一个子网，并将其保存在etcd和本地路径<pre class="crayon-plain-tag">/run/flannel/subnet.env</pre></li>
<li>Kubelet调用Galaxy的CNI插件galaxy-sdn，该插件会通过UDS调用本机的Galaxy进程</li>
<li>Galaxy进程则又调用Flannel CNI，解析/run/flannel/subnet.env中的子网信息</li>
<li>Flannel CNI会调用：
<ol>
<li>Bridge CNI或Veth CNI来为POD配置网络</li>
<li>调用host-lo</li>
</ol>
</li>
</ol>
<p>架构图：</p>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/07/galaxy-overlay.png"><img class="wp-image-33845 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/07/galaxy-overlay.png" alt="galaxy-overlay" width="1013" height="455" /></a></p>
<div class="blog_h3"><span class="graybg">Underlay网络</span></div>
<p>该模式下，容器IP由宿主机网络提供，容器与宿主机可以直接路由，性能更好。<span style="background-color: #c0c0c0;">支持基于Linux Bridge/MacVlan/IPVlan和SRIOV的容器-宿主机二层联通</span>，可以根据业务场景和硬件环境，具体选择使用哪种网桥。</p>
<p>要使用Galaxy Underlay网络，需要启用Galaxy-ipam组件，该组件为Pod分配IP，浮动IP的能力也由它提供。</p>
<div class="blog_h3"><span class="graybg">NAT</span></div>
<p>利用K8S的hostPort配置，将容器端口映射到宿主机端口。如果不指定hostPort，Galaxy进行随机映射。</p>
<div class="blog_h3"><span class="graybg">Host</span></div>
<p>利用K8S的HostNetwork模式，直接使用物理网络。</p>
<div class="blog_h2"><span class="graybg">架构</span></div>
<p>Galaxy有三类组件构成。</p>
<div class="blog_h3"><span class="graybg">Galaxy</span></div>
<p>以DaemonSet方式运行在每个节点上，通过调用各种CNI插件来配置容器网络。</p>
<div class="blog_h3"><span class="graybg">CNI插件</span></div>
<p>Galaxy将实际的创建CNI的工作委托给其它CNI插件，也就是说它扮演一个装饰器的角色。Galaxy支持任何标准CNI插件，它也内置了若干CNI插件。</p>
<div class="blog_h3"><span class="graybg">Galaxy IPAM</span></div>
<p>是一个K8S Scheduler插件。kube-scheduler通过HTTP调用Galaxy-ipam，实现浮动IP的配置和管理。</p>
<p>因为仅仅在重新调度Pod的时候才会面临IP地址变化的问题，因此这个组件实现为Scheduler插件就很自然了。</p>
<div class="blog_h2"><span class="graybg">支持的CNI列表</span></div>
<p>Galaxy支持任何标准的CNI插件，你可以将其用作一个CNI框架，就像<a href="https://github.com/intel/multus-cni">multus-cni</a>那样。</p>
<p>Galaxy还包含了一系列内置的CNI插件：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">插件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>SDN CNI</td>
<td>使用Overlay网络时，此插件是CNI的入口。该插件会通过UDS调用Galaxy守护进程，转发Kubelet提供的所有CNI参数</td>
</tr>
<tr>
<td>Veth CNI</td>
<td>用于Overlay网络中，创建一个VETH对来连接容器和宿主机命名空间。该插件从ipam插件得到Pod的IP</td>
</tr>
<tr>
<td>underlay-veth CNI</td>
<td>用于Underlay网络，创建VETH对来连接容器和宿主机命名空间，该插件不会创建网桥，而是使用宿主机路由规则来进行封包转发</td>
</tr>
<tr>
<td>Vlan CNI</td>
<td>
<p>用于Underlay网络，创建一个VETH对，连接容器和宿主机上的bridge/macvlan/ipvlan设备</p>
<p>此插件支持为Pod配置VLAN，它能够从CNI参数，例如ipinfos=[{"ip":"192.168.0.68/26","vlan":2,"gateway":"192.168.0.65"}] ，或者ipam CNI插件的结果中获得IP地址</p>
</td>
</tr>
<tr>
<td>SRIOV CNI</td>
<td>用于Underlay网络，利用以太网服务器适配器（Ethernet Server Adapter）的SR-IOV，能够创建VF设备并将其放入容器的网络命名空间</td>
</tr>
<tr>
<td>TKE route ENI CNI</td>
<td>用于腾讯云</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">基础</span></div>
<div class="blog_h2"><span class="graybg">构建Galaxy</span></div>
<div class="blog_h3"><span class="graybg">二进制文件</span></div>
<pre class="crayon-plain-tag">go get -d tkestack.io/galaxy
cd $GOPATH/src/tkestack.io/galaxy

# 构建所有二进制文件
make
# 构建指定二进制文件
make BINS="galxy galxy-ipam"
make BINS="galxy-ipam"</pre>
<div class="blog_h3"><span class="graybg">镜像</span></div>
<pre class="crayon-plain-tag"># 构建所有镜像
make image

# 构建指定镜像
make image BINS="galxy-ipam"

# 为指定体系结构构建
make image.multiarch PLATFORMS="linux_arm64" </pre>
<div class="blog_h2"><span class="graybg">使用Galaxy</span></div>
<p>清单文件可以<a href="https://raw.githubusercontent.com/tkestack/galaxy/master/yaml/galaxy.yaml">到GitHub下载</a>，默认内容如下：</p>
<pre class="crayon-plain-tag">---
apiVersion: rbac.authorization.k8s.io/v1
# kubernetes versions before 1.8.0 should use rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: galaxy
rules:
- apiGroups: [""]
  resources:
  - pods
  - namespaces
  - nodes
  - pods/binding
  verbs: ["list", "watch", "get", "patch", "create", "update"]
- apiGroups: ["apps", "extensions"]
  resources:
  - statefulsets
  - deployments
  verbs: ["list", "watch"]
- apiGroups: [""]
  resources:
  - configmaps
  - endpoints
  - events
  verbs: ["get", "list", "watch", "update", "create", "patch"]
- apiGroups: ["galaxy.k8s.io"]
  resources:
  - pools
  - floatingips
  verbs: ["get", "list", "watch", "update", "create", "patch", "delete"]
- apiGroups: ["apiextensions.k8s.io"]
  resources:
  - customresourcedefinitions
  verbs:
  - "*"
- apiGroups: ["networking.k8s.io"]
  resources:
  - networkpolicies
  verbs: ["get", "list", "watch"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: galaxy
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
# kubernetes versions before 1.8.0 should use rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: galaxy
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: galaxy
subjects:
  - kind: ServiceAccount
    name: galaxy
    namespace: kube-system
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    app: galaxy
  name: galaxy
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: galaxy
  template:
    metadata:
      labels:
        app: galaxy
    spec:
      priorityClassName: system-node-critical
      serviceAccountName: galaxy
      hostNetwork: true
      hostPID: true
      containers:
      - image: tkestack/galaxy:v1.0.7
        command: ["/bin/sh"]
# 入口点脚本：
#   拷贝来自ConfigMap的Galaxy配置到宿主机
#   cp -p /etc/galaxy/cni/00-galaxy.conf /etc/cni/net.d/; 
#   拷贝CNI插件二进制文件到宿主机
#   cp -p /opt/cni/galaxy/bin/galaxy-sdn /opt/cni/galaxy/bin/loopback /opt/cni/bin/; 
#   启动Galaxy守护进程                        腾讯云中运行要这个参数
#   /usr/bin/galaxy --logtostderr=true --v=3 --route-eni
      # qcloud galaxy should run with --route-eni
        args: ["-c", "cp -p /etc/galaxy/cni/00-galaxy.conf /etc/cni/net.d/; cp -p /opt/cni/galaxy/bin/galaxy-sdn /opt/cni/galaxy/bin/loopback /opt/cni/bin/; /usr/bin/galaxy --logtostderr=true --v=3 --route-eni"]
      # private-cloud should run without --route-eni
      # args: ["-c", "cp -p /etc/galaxy/cni/00-galaxy.conf /etc/cni/net.d/; cp -p /opt/cni/galaxy/bin/galaxy-sdn /opt/cni/galaxy/bin/loopback /opt/cni/bin/; /usr/bin/galaxy --logtostderr=true --v=3"]
        imagePullPolicy: Always
        env:
          - name: MY_NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName
          - name: DOCKER_HOST
            value: unix:///host/run/docker.sock
        name: galaxy
        resources:
          requests:
            cpu: 100m
            memory: 200Mi
        securityContext:
          privileged: true
        volumeMounts:
        - name: galaxy-run
          mountPath: /var/run/galaxy/
        - name: flannel-run
          mountPath: /run/flannel
        - name: galaxy-etc
          mountPath: /etc/galaxy
        - name: cni-config
          mountPath: /etc/cni/net.d/
        - name: cni-bin
          mountPath: /opt/cni/bin
        - name: cni-etc
          mountPath: /etc/galaxy/cni
        - name: cni-state
          mountPath: /var/lib/cni
        - name: docker-sock
          mountPath: /host/run/
        - name: tz-config
          mountPath: /etc/localtime
      terminationGracePeriodSeconds: 30
      tolerations:
      - operator: Exists
      volumes:
      - name: galaxy-run
        hostPath:
          path: /var/run/galaxy
      - name: flannel-run
        hostPath:
          path: /run/flannel
      - configMap:
          defaultMode: 420
          name: galaxy-etc
        name: galaxy-etc
      - name: cni-config
        hostPath:
          path: /etc/cni/net.d/
      - name: cni-bin
        hostPath:
          path: /opt/cni/bin
      - name: cni-state
        hostPath:
          path: /var/lib/cni
      - configMap:
          defaultMode: 420
          name: cni-etc
        name: cni-etc
      - name: docker-sock
        # in case of docker restart, /run/docker.sock may change, we have to mount the /run directory
        hostPath:
          path: /run/
      - name: tz-config
        hostPath:
          path: /etc/localtime
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: galaxy-etc
  namespace: kube-system
data:
  # Galaxy配置文件
  # update network card name in "galaxy-k8s-vlan" and "galaxy-k8s-sriov" if necessary
  # update vf_num in "galaxy-k8s-sriov" according to demand
  # update ENIIPNetwork to tke-route-eni if running on qcloud
  galaxy.json: |
    {
      "NetworkConf":[
        {"name":"tke-route-eni","type":"tke-route-eni","eni":"eth1","routeTable":1},
        {"name":"galaxy-flannel","type":"galaxy-flannel", "delegate":{"type":"galaxy-veth"},"subnetFile":"/run/flannel/subnet.env"},
        {"name":"galaxy-k8s-vlan","type":"galaxy-k8s-vlan", "device":"eth1", "default_bridge_name": "br0"},
        {"name":"galaxy-k8s-sriov","type": "galaxy-k8s-sriov", "device": "eth1", "vf_num": 10}
      ],
      "DefaultNetworks": ["galaxy-flannel"],
      "ENIIPNetwork": "galaxy-k8s-vlan"
    }
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: cni-etc
  namespace: kube-system
data:
  # CNI网络配置
  00-galaxy.conf: |
    {
      "name": "galaxy-sdn",
      "type": "galaxy-sdn",
      "capabilities": {"portMappings": true},
      "cniVersion": "0.2.0"
    }</pre>
<p>根据实际运行环境、使用的网络模式，需要进行调整。</p>
<div class="blog_h2"><span class="graybg">配置Kubelet</span></div>
<p>需要保证命令行标记：<pre class="crayon-plain-tag">--network-plugin=cni --cni-bin-dir=/opt/cni/bin/</pre>，并重启Kubelet。</p>
<div class="blog_h2"><span class="graybg">Overlay网络</span></div>
<div class="blog_h3"><span class="graybg">安装Flannel</span></div>
<p>这种网络模式依赖于Flannel，参考<a href="/flannel#install">Flannel学习笔记</a>。</p>
<div class="blog_h2"><span class="graybg">配置Galaxy</span></div>
<div class="blog_h3"><span class="graybg">全局配置</span></div>
<p>你需要修改上述Galaxy清单中的galaxy-etc，来配置默认网络（DefaultNetworks）：</p>
<pre class="crayon-plain-tag">{
  // 所有支持的网络模式的配置
  "NetworkConf":[
    {
      "name":"tke-route-eni", // 如果name为空，Galaxy假设它和type相同
      "type":"tke-route-eni",
      "eni":"eth1",
      "routeTable":1
    },
    {
      "name":"galaxy-flannel", // 这个插件就是flannel的CNI，只是被改了名字
      "type":"galaxy-flannel",
      "delegate":{
        "type":"galaxy-veth"
      },
      "subnetFile":"/run/flannel/subnet.env"
    },
    {
      "name":"galaxy-k8s-vlan",
      "type":"galaxy-k8s-vlan",
      "device":"eth1",
      "default_bridge_name":"br0"
    },
    {
      "name":"galaxy-k8s-sriov",
      "type":"galaxy-k8s-sriov",
      "device":"eth1",
      "vf_num":10
    },
    {
      "name":"galaxy-underlay-veth",
      "type":"galaxy-underlay-veth",
      "device":"eth1"
    }
  ],
  // Pod默认使用这个网络，注意可以加入多个网络
  "DefaultNetworks":[
    "galaxy-flannel" // 使用基于Flannel的Overlay网络
  ],
  // 如果Pod需要腾讯云弹性网卡（ENI）而且没有配置k8s.v1.cni.cncf.io/networks注解，则
  // 默认使用此网络。这个配置可以避免需要为所有需要Underlay网络的Pod添加注解
  "ENIIPNetwork":"galaxy-k8s-vlan"
}</pre>
<div class="blog_h3"><span class="graybg">Pod配置</span></div>
<p>可以为Pod指定注解： </p>
<pre class="crayon-plain-tag">k8s.v1.cni.cncf.io/networks: galaxy-flannel,galaxy-k8s-sriov</pre>
<p>来提示Galaxy应该为它配置哪些网络。 </p>
<div class="blog_h2"><span class="graybg">和其它CNI插件共存</span></div>
<p>Galaxy可以和其它CNI插件同时存在。需要注意的一点是，--network-conf-dir目录下其它插件的配置文件，其文件名的字典排序不应该比00-galaxy.conf更小，<span style="background-color: #c0c0c0;">否则Kubelet会在调用Galaxy CNI插件之前，调用其它CNI插件</span>，这可能不符合预期。</p>
<div class="blog_h2"><span class="graybg">Galaxy命令行标记</span></div>
<pre class="crayon-plain-tag">--alsologtostderr                   # 记录日志到文件，同时记录到标准错误
--bridge-nf-call-iptables           # 是否配置 bridge-nf-call-iptables，启用/禁用对网桥转发的封包被iptables规则过滤，默认true
--cni-paths stringSlice             # 除了从kubelet接收的，额外的CNI路径，默认/opt/cni/galaxy/bin
--flannel-allocated-ip-dir string   # Flannel CNI插件在何处存放分配的IP地址，默认/var/lib/cni/networks
--flannel-gc-interval duration      # 执行Flannel网络垃圾回收的间隔，默认10s
--gc-dirs string                    # 清理哪些目录，这些目录中的文件名包含容器ID
                                    # 默认 /var/lib/cni/flannel,/var/lib/cni/galaxy,/var/lib/cni/galaxy/port
--hostname-override string          # 覆盖kubelet hostname，如果指定该参数，则Galaxy使用它从API Server得到节点对象
--ip-forward                        # 是否启用IP转发
--json-config-path string           # Galaxy配置文件路径，默认/etc/galaxy/galaxy.json
--kubeconfig string                 # Kubelet配置文件
--log-backtrace-at traceLocation    # 如果日志在file:N打印，打印栈追踪。默认default :0
--log-dir string                    # 日志输出目录
--log-flush-frequency duration      # 日志刷出间隔，默认5s
--logtostderr                       # 输出到标准错误而非文件
--master string                     # API Server的地址和端口
--network-conf-dir string           # 额外的CNI网络配置文件。默认/etc/cni/net.d/
--network-policy                    # 启用网络策略支持
--route-eni                         # 是否启用腾讯云route-eni
--stderrthreshold severity          # 以上级别的日志打印到标准错误。默认2
-v, --v Level                       # 日志冗长级别 </pre>
<div class="blog_h1"><span class="graybg">Underlay网络</span></div>
<div class="blog_h2"><span class="graybg">工作原理</span></div>
<p>Underlay网络必须配合Galaxy-ipam使用，Galaxy-ipam负责为Pod分配或释放IP地址：</p>
<ol>
<li>你需要规划容器网络中使用的Underlay IP范围（注意和物理网路上其它节点的IP冲突），并且配置到ConfigMap floatingip-config中</li>
<li>调度Pod时，kube-scheduler会在filter/priority/bind方法中调用Galaxy-ipam</li>
<li>Galaxy-ipam检查Pod是否配置了Reserved IP，如果是，则Galaxy-ipam仅将此IP所在的可用子网的节点标记为有效节点，否则所有节点都将被标记为有效节点。在Pod绑定IP期间，Galaxy-ipam分配一个IP并将其写入到Pod annotations中</li>
<li>Galaxy从Pod的注解中获得IP，并将其作为参数传递给CNI，通过CNI配置Pod IP</li>
</ol>
<p>Galaxy的浮动IP能力由Galaxy-ipam提供，后者是一个<a href="https://kubernetes.io/docs/concepts/extend-kubernetes/extend-cluster/#scheduler-extensions">Kubernetes Scheudler Extender</a>，K8S的调度器会调用Galaxy-ipam，影响其filtering/binding的过程。浮动IP必须配合Underlay网络使用。</p>
<p>整体工作流图如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/07/galaxy-ipam.png"><img class="wp-image-33853 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/07/galaxy-ipam.png" alt="galaxy-ipam" width="1007" height="485" /></a></p>
<div class="blog_h2"><span class="graybg">安装Galaxy-ipam</span></div>
<p>清单文件可以到GitHub下载，默认内容如下：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Service
metadata:
  name: galaxy-ipam
  namespace: kube-system
  labels:
    app: galaxy-ipam
spec:
  type: NodePort
  ports:
  - name: scheduler-port
    port: 9040
    targetPort: 9040
    nodePort: 32760
    protocol: TCP
  - name: api-port
    port: 9041
    targetPort: 9041
    nodePort: 32761
    protocol: TCP
  selector:
    app: galaxy-ipam
---
apiVersion: rbac.authorization.k8s.io/v1
# kubernetes versions before 1.8.0 should use rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: galaxy-ipam
rules:
- apiGroups: [""]
  resources:
  - pods
  - namespaces
  - nodes
  - pods/binding
  verbs: ["list", "watch", "get", "patch", "create"]
- apiGroups: ["apps", "extensions"]
  resources:
  - statefulsets
  - deployments
  verbs: ["list", "watch"]
- apiGroups: [""]
  resources:
  - configmaps
  - endpoints
  - events
  verbs: ["get", "list", "watch", "update", "create", "patch"]
- apiGroups: ["galaxy.k8s.io"]
  resources:
  - pools
  - floatingips
  verbs: ["get", "list", "watch", "update", "create", "patch", "delete"]
- apiGroups: ["apiextensions.k8s.io"]
  resources:
  - customresourcedefinitions
  verbs:
  - "*"
- apiGroups: ["apps.tkestack.io"]
  resources:
  - tapps
  verbs: ["list", "watch"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: galaxy-ipam
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
# kubernetes versions before 1.8.0 should use rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: galaxy-ipam
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: galaxy-ipam
subjects:
  - kind: ServiceAccount
    name: galaxy-ipam
    namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: galaxy-ipam
  name: galaxy-ipam
  namespace: kube-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app: galaxy-ipam
  template:
    metadata:
      labels:
        app: galaxy-ipam
    spec:
      priorityClassName: system-cluster-critical
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - galaxy-ipam
            topologyKey: "kubernetes.io/hostname"
      serviceAccountName: galaxy-ipam
      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet
      containers:
      - image: tkestack/galaxy-ipam:v1.0.7
        args:
          - --logtostderr=true
          - --profiling
          - --v=3
          - --config=/etc/galaxy/galaxy-ipam.json
          # 这个服务暴露一个端口，供Scheduler调用
          - --port=9040
          - --api-port=9041
          - --leader-elect
        command:
          - /usr/bin/galaxy-ipam
        ports:
          - containerPort: 9040
          - containerPort: 9041
        imagePullPolicy: Always
        name: galaxy-ipam
        resources:
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: kube-config
          mountPath: /etc/kubernetes/
        - name: galaxy-ipam-log
          mountPath: /data/galaxy-ipam/logs
        - name: galaxy-ipam-etc
          mountPath: /etc/galaxy
        - name: tz-config
          mountPath: /etc/localtime
      terminationGracePeriodSeconds: 30
      tolerations:
        - effect: NoSchedule
          key: node-role.kubernetes.io/master
          operator: Exists
      volumes:
      - name: kube-config
        hostPath:
          path: /etc/kubernetes/
      - name: galaxy-ipam-log
        emptyDir: {}
      - configMap:
          defaultMode: 420
          name: galaxy-ipam-etc
        name: galaxy-ipam-etc
      - name: tz-config
        hostPath:
          path: /etc/localtime
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: galaxy-ipam-etc
  namespace: kube-system
data:
  # 配置文件
  galaxy-ipam.json: |
    {
      "schedule_plugin": {
        # 如果不是使用腾讯云的弹性网卡（ENI），去掉这一行
        "cloudProviderGrpcAddr": "127.0.0.2:80"
      }
    }</pre>
<div class="blog_h2"><span class="graybg">配置kube-scheduler</span></div>
<p>你需要为K8S调度器配置调度策略，此策略现在可以放在ConfigMap中：</p>
<pre class="crayon-plain-tag">cat &lt;&lt;EOF | kubectl create -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: scheduler-policy
  namespace: kube-system
data:
  policy.cfg: |
    {
      "kind": "Policy",
      "apiVersion": "v1",
      "extenders": [
        {
          // 通过HTTP调用扩展
          "urlPrefix": "http://127.0.0.1:9040/v1",
          "httpTimeout": 10000000000,
          "filterVerb": "filter",
          "BindVerb": "bind",
          "weight": 1,
          "enableHttps": false,
          "managedResources": [
            {
              "name": "tke.cloud.tencent.com/eni-ip",
              "ignoredByScheduler": false
            }
          ]
        }
      ]
    }
EOF</pre>
<p>然后为调度器添加命令行标记：<pre class="crayon-plain-tag">--policy-configmap=scheduler-policy</pre>即可。</p>
<div class="blog_h2"><span class="graybg">配置浮动IP</span></div>
<div class="blog_h3"><span class="graybg">floatingip-config</span></div>
<p>运行在裸金属环境下时，可以使用如下配置：</p>
<pre class="crayon-plain-tag">kind: ConfigMap
apiVersion: v1
metadata:
 name: floatingip-config
 namespace: kube-system
data:
 floatingips: |
    [
        { 
            # 节点的CIDR，这个网络范围内的节点可以运行具有浮动IP的Pod
            "nodeSubnets": ["10.0.0.0/16"],
            # 可以分配给Pod的IP地址范围
            "ips": ["10.0.70.2~10.0.70.241"],
            # Pod IP子网信息
            "subnet":"10.0.70.0/24",
            # Pod IP网关信息
            "gateway":"10.0.70.1",
            # Pod IP所属VLAN，如果Pod IP和Node IP不在同一VLAN，需要设置该字段，同时
            # 确保节点所连接的交换机时一个trunk port
            "vlan": 1024
        }
    ]</pre>
<p>一个nodeSubnet可以对应多个Pod Subnet，例如：</p>
<pre class="crayon-plain-tag">// 如果Pod运行在10.49.28.0/26，那么它可以具有10.0.80.2~10.0.80.4或者10.0.81.2~10.0.81.4的IP地址
// 如果Pod运行在10.49.29.0/24，则只能具有10.0.80.0/24的IP地址
[{
	"nodeSubnets": ["10.49.28.0/26", "10.49.29.0/24"],
	"ips": ["10.0.80.2~10.0.80.4"],
	"subnet": "10.0.80.0/24",
	"gateway": "10.0.80.1"
}, {
	"nodeSubnets": ["10.49.28.0/26"],
	"ips": ["10.0.81.2~10.0.81.4"],
	"subnet": "10.0.81.0/24",
	"gateway": "10.0.81.1",
	"vlan": 3
}]</pre>
<p>只要可分配的IP地址范围不重叠，多个nodeSubnet可以共享同一个Pod Subnet：</p>
<pre class="crayon-plain-tag">[{
	"routableSubnet": "10.180.1.2/32",
	"ips": ["10.180.154.2~10.180.154.3"],
	"subnet": "10.180.154.0/24",
	"gateway": "10.180.154.1",
	"vlan": 3
}, {
	"routableSubnet": "10.180.1.3/32",
	"ips": ["10.180.154.7~10.180.154.8"],
	"subnet": "10.180.154.0/24",
	"gateway": "10.180.154.1",
	"vlan": 3
}]</pre>
<div class="blog_h3"><span class="graybg">保留IP</span></div>
<p>要保留一个IP不被分配，可以使用下面的FloatingIP资源： </p>
<pre class="crayon-plain-tag">apiVersion: galaxy.k8s.io/v1alpha1
kind: FloatingIP
metadata:
  # 名字是需要保留的IP
  name: 10.0.0.1
  labels:
    # 从1.0.8下面这个标签不再需要
    ipType: internalIP
    # 这个标签必须保留
    reserved: this-is-not-for-pods
spec:
  key: pool__reserved-for-node_
  policy: 2</pre>
<div class="blog_h3"><span class="graybg">Deployment配置</span></div>
<p>目前浮动IP仅仅支持Deployment、StatefulSet产生的工作负载。</p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: Deployment
...
spec:
  ...
  template:
    metadata:
      annotations:
        # 如果默认网络不是galaxy-k8s-vlan则必须添加下面的注解
        k8s.v1.cni.cncf.io/networks: galaxy-k8s-vlan
        # IP释放策略：
        #    为空/不指定：Pod一旦停止，就释放IP
        #    immutable：仅仅在删除、缩容Deployment / StatefulSet的情况下才释放IP
        #    never：即使Deployment / StatefulSet被删除，也不会释放IP。后续的同名Deployment
        #           /StatefulSet会重用已分配的IP
        k8s.v1.cni.galaxy.io/release-policy: immutable
      creationTimestamp: null
      labels:
        k8s-app: nnn
        qcloud-app: nnn
    spec:
      containers:
      - image: nginx
        imagePullPolicy: Always
        name: nnn
        resources:
          limits:
            cpu: 500m
            memory: 1Gi
            # 扩展的资源限制，在某个容器中配置即可
            tke.cloud.tencent.com/eni-ip: "1"
          requests:
            cpu: 250m
            memory: 256Mi
            tke.cloud.tencent.com/eni-ip: "1"</pre>
<p>生成的Pod中，通过注解<pre class="crayon-plain-tag">k8s.v1.cni.galaxy.io/args</pre>指明了它的IP地址等信息。CNI插件也是基于这些信息为容器网络命名空间设置IP地址、路由的：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: nnn-7df5984746-58hjm
  annotations:
    k8s.v1.cni.cncf.io/networks: galaxy-k8s-vlan
    k8s.v1.cni.galaxy.io/args: '{"common":{"ipinfos":[{"ip":"192.168.64.202/24",
        "vlan":0,"gateway":"192.168.64.1","routable_subnet":"172.21.64.0/20"}]}}'
    k8s.v1.cni.galaxy.io/release-policy: immutable
...
spec:
...
status:
...
  hostIP: 172.21.64.15
  phase: Running
  podIP: 192.168.64.202
  podIPs:
  - ip: 192.168.64.202</pre>
<div class="blog_h2"><span class="graybg">Galaxy-ipam相关CRD</span></div>
<div class="blog_h3"><span class="graybg">Pool</span></div>
<p>为Deployment设置注解<pre class="crayon-plain-tag">tke.cloud.tencent.com/eni-ip-pool</pre>，可以让多个Deployment共享一个IP池。</p>
<p>使用IP池的情况下，默认的IP释放策略为never，除非通过注解<pre class="crayon-plain-tag">k8s.v1.cni.galaxy.io/release-policy</pre>指定其它策略。</p>
<p>默认情况下，IP池的大小随着Deployment/StatefulSet的副本数量的增加而增大，你可以设置固定尺寸的IP池：</p>
<pre class="crayon-plain-tag">apiVersion: galaxy.k8s.io/v1alpha1
kind: Pool
metadata:
  name: example-pool
size: 4</pre>
<p>通过HTTP接口创建IP池的时候，可以设置preAllocateIP=true来为池预先分配IP，通过kubectl创建CR时无法实现预分配。 </p>
<div class="blog_h3"><span class="graybg">FloatingIP</span></div>
<p>该CR保存了浮动IP，及其绑定的工作负载信息：</p>
<pre class="crayon-plain-tag">apiVersion: galaxy.k8s.io/v1alpha1
kind: FloatingIP
metadata:
  creationTimestamp: "2020-03-04T08:28:15Z"
  generation: 1
  labels:
    ipType: internalIP
  # IP地址
  name: 192.168.64.202
  resourceVersion: "2744910"
  selfLink: /apis/galaxy.k8s.io/v1alpha1/floatingips/192.168.64.202
  uid: b5d55f27-4548-44c7-b8ad-570814b55026
spec:
  attribute: '{"NodeName":"172.21.64.15"}'
  # 绑定的工作负载
  key: dp_default_nnn_nnn-7df5984746-58hjm
  policy: 1
  subnet: 172.21.64.0/20
  updateTime: "2020-03-04T08:28:15Z" </pre>
<div class="blog_h2"><span class="graybg">Galaxy-ipam HTTP接口</span></div>
<p>Galaxy-ipam提供了基于<a href="https://github.com/tkestack/galaxy/blob/master/doc/swagger.json">Swagger 2.0</a>的API，为galaxy-ipam提供命令行选项<pre class="crayon-plain-tag">--swagger</pre>，则它能够在下面的URL展示此API：</p>
<p style="padding-left: 30px;">http://${galaxy-ipam-ip}:9041/apidocs.json/v1</p>
<div class="blog_h3"><span class="graybg">查询分配的IP</span></div>
<pre class="crayon-plain-tag">// 查询分配给default命名空间中的StatefulSet sts的IP地址
// curl 'http://192.168.30.7:9041/v1/ip?appName=sts&amp;appType=statefulsets&amp;namespace=default'
{
 "last": true,
 "totalElements": 2,
 "totalPages": 1,
 "first": true,
 "numberOfElements": 2,
 "size": 10,
 "number": 0,
 "content": [
  {
   "ip": "10.0.0.112",
   "namespace": "default",
   "appName": "sts",
   "podName": "sts-0",
   "policy": 2,
   "appType": "statefulset",
   "updateTime": "2020-05-29T11:11:44.633383558Z",
   "status": "Deleted",
   "releasable": true
  },
  {
   "ip": "10.0.0.174",
   "namespace": "default",
   "appName": "sts",
   "podName": "sts-1",
   "policy": 2,
   "appType": "statefulset",
   "updateTime": "2020-05-29T11:11:45.132450117Z",
   "status": "Deleted",
   "releasable": true
  }
 ]
}</pre>
<div class="blog_h3"><span class="graybg">释放IP地址</span></div>
<pre class="crayon-plain-tag">// curl -X POST -H "Content-type: application/json" -d '
//   { "ips": [
//     { "ip":"10.0.0.112", "appName":"sts", "appType":"statefulset",  "podName":"sts-0","namespace":"default"},
//     {"ip":"10.0.0.174", "appName":"sts", "appType":"statefulset", "podName":"sts-1", "namespace":"default"}
//   ]}
// '
// http://192.168.30.7:9041/v1/ip
{
 "code": 200,
 "message": ""
}</pre>
<div class="blog_h2"><span class="graybg">常见问题</span></div>
<div class="blog_h3"><span class="graybg">关于滚动更新</span></div>
<p>Deployment的默认更新策略是StrategyType=RollingUpdate，25% max unavailable, 25% max surge。这意味着这在滚动更新过程中，可能存在超过副本数限制25%的Pod。这可能导致死锁状态：</p>
<ol>
<li>假设Deployment副本数为3，那么有一个副本不可用，就超过25%的限制。Deployment的控制器只能先创建新Pod，然后等它就绪后再删除一个旧Pod</li>
<li>如果浮动IP的释放策略被设置为immutable/never，这就意味着新的Pod需要从一个旧Pod那复用IP，然而旧Pod还尚未删除</li>
</ol>
<p>这会导致新Pod卡斯在调度阶段。</p>
<div class="blog_h1"><span class="graybg">端口映射</span></div>
<p>使用K8S的NodePort服务，可以将一组Pod通过宿主机端口暴露到集群外部。但是如果希望访问StatefulSet的每个特定Pod的端口，使用K8S NodePort服务无法实现。</p>
<p>使用K8S的HostPort，则会存在两个Pod调度到同一节点，进而产生端口冲突的问题：</p>
<pre class="crayon-plain-tag">spec:
      containers:
      - image: ...
        ports:
          - containerPort: 9040
            hostPort: 9040</pre>
<p>Galaxy提供了一个功能，可以进行随机的端口映射： </p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: hello
  name: hello
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
      annotations:
        # 为Pod设置此注解即可
        tkestack.io/portmapping: ""
    spec:
      containers:
      - image: ...
        ports:
          - containerPort: 9040</pre>
<p>这样Galaxy会利用iptables，随机的映射宿主机的端口到Pod的每个容器端口，并且将此随机端口回填到tkestack.io/portmapping注解中。</p>
<div class="blog_h1"><span class="graybg">源码解读</span></div>
<div class="blog_h2"><span class="graybg">CNI</span></div>
<div class="blog_h3"><span class="graybg">galaxy-sdn</span></div>
<p>在TKEStack的缺省配置（基于Flannel的VXLAN模式的Overlay网络）下，每个节点只有一个CNI配置：</p>
<pre class="crayon-plain-tag">{
  "type": "galaxy-sdn",
  "capabilities": {"portMappings": true},
  "cniVersion": "0.2.0"
}</pre>
<p>我们先看看这个CNI插件做的什么事情，又是如何和Flannel交互的。Galaxy所有自带的CNI插件都位于github.com/tkestack/galaxy/cni目录下，其中galaxy-sdn插件入口点如下：</p>
<pre class="crayon-plain-tag">package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"os"
	"strings"

	"github.com/containernetworking/cni/pkg/skel"
	// 使用0.20版本的CNI
	t020 "github.com/containernetworking/cni/pkg/types/020"
	"github.com/containernetworking/cni/pkg/version"
	galaxyapi "tkestack.io/galaxy/pkg/api/galaxy"
	"tkestack.io/galaxy/pkg/api/galaxy/private"
)

type cniPlugin struct {
	socketPath string
}

func NewCNIPlugin(socketPath string) *cniPlugin {
	return &amp;cniPlugin{socketPath: socketPath}
}

// Create and fill a CNIRequest with this plugin's environment and stdin which
// contain the CNI variables and configuration
func newCNIRequest(args *skel.CmdArgs) *galaxyapi.CNIRequest {
	envMap := make(map[string]string)
	for _, item := range os.Environ() {
		idx := strings.Index(item, "=")
		if idx &gt; 0 {
			envMap[strings.TrimSpace(item[:idx])] = item[idx+1:]
		}
	}

	return &amp;galaxyapi.CNIRequest{
		Env:    envMap,
		Config: args.StdinData,
	}
}

// 调用Galaxy守护进程
func (p *cniPlugin) doCNI(url string, req *galaxyapi.CNIRequest) ([]byte, error) {
	data, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal CNI request %v: %v", req, err)
	}
	// 通过UDS连接
	client := &amp;http.Client{
		Transport: &amp;http.Transport{
			Dial: func(proto, addr string) (net.Conn, error) {
				return net.Dial("unix", p.socketPath)
			},
		},
	}

	resp, err := client.Post(url, "application/json", bytes.NewReader(data))
	if err != nil {
		return nil, fmt.Errorf("failed to send CNI request: %v", err)
	}
	defer resp.Body.Close() // nolint: errcheck

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read CNI result: %v", err)
	}

	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("galaxy returns: %s", string(body))
	}

	return body, nil
}

// 就是将CNI请求转发给Galaxy守护进程处理
func (p *cniPlugin) CmdAdd(args *skel.CmdArgs) (*t020.Result, error) {
	// 由于通过UDS通信，URL的hostname部分随便写了一个dummy，就是简单的调用/cni这个Endpoint
	body, err := p.doCNI("http://dummy/cni", newCNIRequest(args))
	if err != nil {
		return nil, err
	}

	result := &amp;t020.Result{}
	if err := json.Unmarshal(body, result); err != nil {
		return nil, fmt.Errorf("failed to unmarshal response '%s': %v", string(body), err)
	}

	return result, nil
}

// Send the ADD command environment and config to the CNI server, printing
// the IPAM result to stdout when called as a CNI plugin
func (p *cniPlugin) skelCmdAdd(args *skel.CmdArgs) error {
	result, err := p.CmdAdd(args)
	if err != nil {
		return err
	}
	return result.Print()
}

// Send the DEL command environment and config to the CNI server
func (p *cniPlugin) CmdDel(args *skel.CmdArgs) error {
	_, err := p.doCNI("http://dummy/cni", newCNIRequest(args))
	return err
}

// 使用skel框架开发
func main() {
	// 传入UDS套接字的路径
	p := NewCNIPlugin(private.GalaxySocketPath)
	skel.PluginMain(p.skelCmdAdd, p.CmdDel, version.Legacy)
}</pre>
<p>可以看到galaxy-sdn就是个空壳子，所有工作都是委托给Galaxy守护进程处理的。 </p>
<div class="blog_h3"><span class="graybg">galaxy-k8s-vlan</span></div>
<p>整体流程：</p>
<pre class="crayon-plain-tag">func init() {
	// this ensures that main runs only on main thread (thread group leader).
	// since namespace ops (unshare, setns) are done for a single thread, we
	// must ensure that the goroutine does not jump from OS thread to thread
	runtime.LockOSThread()
	_, pANet, _ = net.ParseCIDR("10.0.0.0/8")
	_, pBNet, _ = net.ParseCIDR("172.16.0.0/12")
	_, pCNet, _ = net.ParseCIDR("192.168.0.0/16")
}

func main() {
	d = &amp;vlan.VlanDriver{}
	skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

func cmdAdd(args *skel.CmdArgs) error {
	// 加载CNI配置文件
	conf, err := d.LoadConf(args.StdinData)
	if err != nil {
		return err
	}
	// 首先尝试从args获得IPInfo，如果失败则调用第三方IPAM插件分配IP
	vlanIds, results, err := ipam.Allocate(conf.IPAM.Type, args)
	if err != nil {
		return err
	}
	// 判断是否创建默认网桥
	if d.DisableDefaultBridge == nil {
		defaultTrue := true
		d.DisableDefaultBridge = &amp;defaultTrue
		for i := range vlanIds {
			if vlanIds[i] == 0 {
				*d.DisableDefaultBridge = false
			}
		}
	}
	// 初始化VLAN驱动，主要是在必要的情况下对宿主机上的网桥进行配置，或者进行针对MACVlan/IPVlan的配置
	if err := d.Init(); err != nil {
		return fmt.Errorf("failed to setup bridge %v", err)
	}
	result020s, err := resultConvert(results)
	if err != nil {
		return err
	}
	// 配置宿主机网络
	if err := setupNetwork(result020s, vlanIds, args); err != nil {
		return err
	}
	result020s[0].DNS = conf.DNS
	return result020s[0].Print()
}

func cmdDel(args *skel.CmdArgs) error {
	// 删除所有VETH对
	if err := utils.DeleteAllVeth(args.Netns); err != nil {
		return err
	}
	conf, err := d.LoadConf(args.StdinData)
	if err != nil {
		return err
	}
	// 是否IP地址
	return ipam.Release(conf.IPAM.Type, args)
}</pre>
<p>VlanDriver是此CNI的核心。 </p>
<pre class="crayon-plain-tag">type VlanDriver struct {
	//FIXME add a file lock cause we are running multiple processes?
	*NetConf
	// 不管是物理接口，还是VLAN子接口，都位于宿主机命名空间
	// 所有VLAN接口的物理接口（父接口）的设备索引，例如eth1
	vlanParentIndex int
	// 如果启用VLAN，则是当前所属VLAN的子接口的索引。否则是物理接口的索引
	DeviceIndex int
}</pre>
<p>ADD/DEL命令都需要调用下面的方法加载CNI配置：</p>
<pre class="crayon-plain-tag">const (
	VlanPrefix        = "vlan"
	BridgePrefix      = "docker"
	DefaultBridge     = "docker"
	// IPVLAN有两种模式L2/L3
	DefaultIPVlanMode = "l3"
)

func (d *VlanDriver) LoadConf(bytes []byte) (*NetConf, error) {
	conf := &amp;NetConf{}
	if err := json.Unmarshal(bytes, conf); err != nil {
		return nil, fmt.Errorf("failed to load netconf: %v", err)
	}
	if conf.DefaultBridgeName == "" {
		conf.DefaultBridgeName = DefaultBridge
	}
	if conf.BridgeNamePrefix == "" {
		conf.BridgeNamePrefix = BridgePrefix
	}
	if conf.VlanNamePrefix == "" {
		conf.VlanNamePrefix = VlanPrefix
	}
	if conf.IpVlanMode == "" {
		conf.IpVlanMode = DefaultIPVlanMode
	}

	d.NetConf = conf
	return conf, nil
}</pre>
<p>CNI配置定义如下：</p>
<pre class="crayon-plain-tag">type NetConf struct {
	// CNI标准网络配置
	types.NetConf
	// 持有IDC IP地址的设备名，例如eth1或eth1.12（VLAN子接口）
	Device string `json:"device"`
	// 上述Device为空的时候，候选设备列表。取第一个存在的设备
	Devices []string `json:"devices"`
	// 交换机实现方式 macvlan, bridge（默认）, pure（避免创建不必要的网桥）
	Switch string `json:"switch"`
	// IPVLAN模式，可选l2, l3（默认）, l3s
	IpVlanMode string `json:"ipvlan_mode"`

	// 不去创建默认网桥
	DisableDefaultBridge *bool `json:"disable_default_bridge"`
	// 默认网桥的名字
	DefaultBridgeName string `json:"default_bridge_name"`
	// 网桥名字前缀
	BridgeNamePrefix string `json:"bridge_name_prefix"`
	// VLAN接口名字前缀
	VlanNamePrefix string `json:"vlan_name_prefix"`
	// 是否启用免费ARP
	GratuitousArpRequest bool `json:"gratuitous_arp_request"`
	// 设置MTU
	MTU int `json:"mtu"`
}</pre>
<p>ADD命令，加载完CNI配置后，首先是调用IPAM进行IP地址分配：</p>
<pre class="crayon-plain-tag">func Allocate(ipamType string, args *skel.CmdArgs) ([]uint16, []types.Result, error) {
	var (
		vlanId uint16
		err    error
	)
	// 解析key1=val1;key2=val2格式的CNI参数。这些参数来自Pod的k8s.v1.cni.galaxy.io/args注解，
	// 如果使用浮动IP，则Galaxy IPAM会设置该注解，将IP地址填写在ipinfos字段
	kvMap, err := cniutil.ParseCNIArgs(args.Args)
	if err != nil {
		return nil, nil, err
	}
	var results []types.Result
	var vlanIDs []uint16
	// 读取ipinfos字段
	if ipInfoStr := kvMap[constant.IPInfosKey]; ipInfoStr != "" {
		// 解析IP信息
		var ipInfos []constant.IPInfo
		if err := json.Unmarshal([]byte(ipInfoStr), &amp;ipInfos); err != nil {
			return nil, nil, fmt.Errorf("failed to unmarshal ipInfo from args %q: %v", args.Args, err)
		}
		if len(ipInfos) == 0 {
			return nil, nil, fmt.Errorf("empty ipInfos")
		}
		for j := range ipInfos {
			results = append(results, cniutil.IPInfoToResult(&amp;ipInfos[j]))
			vlanIDs = append(vlanIDs, ipInfos[j].Vlan)
		}
		// 直接“分配”Pod注解里写的IP地址
		return vlanIDs, results, nil
	}
	// 如果Pod注解里面没有IP地址信息，则需要调用谋者IPAM插件，因此断言ipamType不为空：
	if ipamType == "" {
		return nil, nil, fmt.Errorf("neither ipInfo from cni args nor ipam type from netconf")
	}
	// 调用IPAM插件
	generalResult, err := ipam.ExecAdd(ipamType, args.StdinData)
	if err != nil {
		return nil, nil, err
	}
	result, err := t020.GetResult(generalResult)
	if err != nil {
		return nil, nil, err
	}
	if result.IP4 == nil {
		return nil, nil, fmt.Errorf("IPAM plugin returned missing IPv4 config")
	}
	return append(vlanIDs, vlanId), append(results, generalResult), err
}</pre>
<p>如果使用Galaxy IPAM提供的浮动IP功能，则上述代码会自动获取已经分配的IP信息并返回。</p>
<p>分配/获取IP地址之后，就执行VlanDriver的初始化：</p>
<pre class="crayon-plain-tag">func (d *VlanDriver) Init() error {
	var (
		device netlink.Link
		err    error
	)
	// 这个驱动需要指定宿主机上的父接口
	if d.Device != "" {
		device, err = netlink.LinkByName(d.Device)
	} else {
		if len(d.Devices) == 0 {
			return fmt.Errorf("a device is needed to use vlan plugin")
		}
		for _, devName := range d.Devices {
			device, err = netlink.LinkByName(devName)
			if err == nil {
				break
			}
		}
	}
	if err != nil {
		return fmt.Errorf("Error getting device %s: %v", d.Device, err)
	}
	// 默认MTU从父接口上查找
	if d.MTU == 0 {
		d.MTU = device.Attrs().MTU
	}
	d.DeviceIndex = device.Attrs().Index
	d.vlanParentIndex = device.Attrs().Index
	// 如果指定的设备是VLAN子接口，则使用其父接口的索引
	if device.Type() == "vlan" {
		//A vlan device
		d.vlanParentIndex = device.Attrs().ParentIndex
		//glog.Infof("root device %s is a vlan device, parent index %d", d.Device, d.vlanParentIndex)
	}
	if d.IPVlanMode() {
		switch d.GetIPVlanMode() {
		case netlink.IPVLAN_MODE_L3S, netlink.IPVLAN_MODE_L3:
			// 允许绑定到非本地地址，什么作用
			return utils.EnableNonlocalBind()
		default:
			return nil
		}
	} else if d.MacVlanMode() {
		return nil
	} else if d.PureMode() {
		if err := d.initPureModeArgs(); err != nil {
			return err
		}
		return utils.EnableNonlocalBind()
	}
	if d.DisableDefaultBridge != nil &amp;&amp; *d.DisableDefaultBridge {
		return nil
	}
	// 需要创建默认网桥
	// 获得父接口的IP地址
	v4Addr, err := netlink.AddrList(device, netlink.FAMILY_V4)
	if err != nil {
		return fmt.Errorf("Errror getting ipv4 address %v", err)
	}
	filteredAddr := network.FilterLoopbackAddr(v4Addr)
	if len(filteredAddr) == 0 {
		// 父接口没有IP地址，那么地址应当转给网桥了。检查确保网桥设备存在
		bri, err := netlink.LinkByName(d.DefaultBridgeName)
		if err != nil {
			return fmt.Errorf("Error getting bri device %s: %v", d.DefaultBridgeName, err)
		}
		if bri.Attrs().Index != device.Attrs().MasterIndex {
			return fmt.Errorf("No available address found on device %s", d.Device)
		}
	} else {
		// 否则，意味着网桥应该还不存在，初始化之
		if err := d.initVlanBridgeDevice(device, filteredAddr); err != nil {
			return err
		}
	}
	return nil
}

// 不使用MacVlan/IPVlan，则需要依赖网桥
func (d *VlanDriver) initVlanBridgeDevice(device netlink.Link, filteredAddr []netlink.Addr) error {
	// 创建网桥
	bri, err := getOrCreateBridge(d.DefaultBridgeName, device.Attrs().HardwareAddr)
	if err != nil {
		return err
	}
	// 启动网桥
	if err := netlink.LinkSetUp(bri); err != nil {
		return fmt.Errorf("failed to set up bridge device %s: %v", d.DefaultBridgeName, err)
	}
	// 获取父接口路由列表
	rs, err := netlink.RouteList(device, nl.FAMILY_V4)
	if err != nil {
		return fmt.Errorf("failed to list route of device %s", device.Attrs().Name)
	}
	defer func() {
		if err != nil {
			// 如果出现错误，则路由需要加回去
			for i := range rs {
				_ = netlink.RouteAdd(&amp;rs[i])
			}
		}
	}()
	// 将父接口的IP地址、路由，转移到网桥上
	err = d.moveAddrAndRoute(device, bri, filteredAddr, rs)
	if err != nil {
		return err
	}
	return nil
}</pre>
<p>可以看到，如果使用MacVlan或者IPVlan模式，则不会去管理宿主机上的网桥（这也是MacVlan/IPVlan的价值之一，不需要网桥）。否则，会创建网桥并转移走父接口的IP地址、路由。</p>
<p>最后，下面的函数负责配置好网络：  </p>
<pre class="crayon-plain-tag">func setupNetwork(result020s []*t020.Result, vlanIds []uint16, args *skel.CmdArgs) error {
	if d.MacVlanMode() {
		// 处理MACVlan模式
		if err := setupMacvlan(result020s[0], vlanIds[0], args); err != nil {
			return err
		}
	} else if d.IPVlanMode() {
		// 处理IPVlan模式
		if err := setupIPVlan(result020s[0], vlanIds[0], args); err != nil {
			return err
		}
	} else {
		// 处理网桥模式
		ifName := args.IfName
		if err := setupVlanDevice(result020s, vlanIds, args); err != nil {
			return err
		}
		args.IfName = ifName
	}
	// 发送一个免费ARP，让交换机知道浮动IP漂移到当前节点上了
	if d.PureMode() {
		_ = utils.SendGratuitousARP(d.Device, result020s[0].IP4.IP.IP.String(), "", d.GratuitousArpRequest)
	}
	return nil
}</pre>
<p>如果使用MACVlan，则配置过程如下：</p>
<pre class="crayon-plain-tag">func setupMacvlan(result *t020.Result, vlanId uint16, args *skel.CmdArgs) error {
	if err := d.MaybeCreateVlanDevice(vlanId); err != nil {
		return err
	}
	// 通过MACVlan连接宿主机和容器
	if err := utils.MacVlanConnectsHostWithContainer(result, args, d.DeviceIndex, d.MTU); err != nil {
		return err
	}
	// 在命名空间内进行宣告
	_ = utils.SendGratuitousARP(args.IfName, result.IP4.IP.IP.String(), args.Netns, d.GratuitousArpRequest)
	return nil
}

func MacVlanConnectsHostWithContainer(result *t020.Result, args *skel.CmdArgs, parent int, mtu int) error {
	var err error
	// 创建MACVlan设备
	macVlan := &amp;netlink.Macvlan{
		Mode: netlink.MACVLAN_MODE_BRIDGE,
		LinkAttrs: netlink.LinkAttrs{
			Name:        HostMacVlanName(args.ContainerID),
			MTU:         mtu,
			ParentIndex: parent, // 父设备可能是物理接口或者VLAN子接口
		}}
	// 添加接口
	if err := netlink.LinkAdd(macVlan); err != nil {
		return err
	}
	// 出错时必须接口被删除
	defer func() {
		if err != nil {
			netlink.LinkDel(macVlan)
		}
	}()
	// 配置沙盒，细节参考下文
	if err = configSboxDevice(result, args, macVlan); err != nil {
		return err
	}
	return nil
}</pre>
<p>如果使用IPVlan，则配置过程如下： </p>
<pre class="crayon-plain-tag">func setupIPVlan(result *t020.Result, vlanId uint16, args *skel.CmdArgs) error {
	// 如有必要，创建VLAN设备
	if err := d.MaybeCreateVlanDevice(vlanId); err != nil {
		return err
	}
	// 连接宿主机和容器网络                                         IPVLAN的父设备可能是
	//                                                            VLAN子接口（位于宿主机命名空间）
	if err := utils.IPVlanConnectsHostWithContainer(result, args, d.DeviceIndex, d.GetIPVlanMode(), d.MTU); err != nil {
		return err
	}

	// l3模式下，所有连接在一起的容器之间的相互通信，都要通过宿主机上的IPVlan父接口进行代理
	// 在我们的环境中（部署在OpenStack的虚拟机上），出现Pod无法ping二层网关的情况，需要
	//     arping -c 2 -U -I eth0 &lt;pod-ip&gt;
	// 才可以解决，等价于下面的代码
	// Gratuitous ARP不能保证ARP缓存永久有效，底层交换机（OpenStack虚拟的）需要进行适当的配置，
	// 将Pod IP和VM的MAC地址关联
	if d.IpVlanMode == "l3" || d.IpVlanMode == "l3s" {
		_ = utils.SendGratuitousARP(d.Device, result.IP4.IP.IP.String(), "", d.GratuitousArpRequest)
		return nil
	}
	// 在网络命名空间中进行宣告
	_ = utils.SendGratuitousARP(args.IfName, result.IP4.IP.IP.String(), args.Netns, d.GratuitousArpRequest)
	return nil
}


// 创建VLAN设备的细节
func (d *VlanDriver) MaybeCreateVlanDevice(vlanId uint16) error {
	// 如果不启用VLAN支持，则不做任何事情
	if vlanId == 0 {
		return nil
	}
	_, err := d.getOrCreateVlanDevice(vlanId)
	return err
}
func (d *VlanDriver) getOrCreateVlanDevice(vlanId uint16) (netlink.Link, error) {
	// 根据父接口和VLAN ID查找已存在的VLAN设备
	link, err := d.getVlanIfExist(vlanId)
	if err != nil || link != nil {
		if link != nil {
			// 设置VLAN设备索引
			d.DeviceIndex = link.Attrs().Index
		}
		return link, err
	}
	vlanIfName := fmt.Sprintf("%s%d", d.VlanNamePrefix, vlanId)
	// 获取VLAN设备，如果获取不到则通过后面的回调函数创建
	vlan, err := getOrCreateDevice(vlanIfName, func(name string) error {
		// 创建设备                                                 名字 vlan100 父接口索引
		vlanIf := &amp;netlink.Vlan{LinkAttrs: netlink.LinkAttrs{Name: vlanIfName, ParentIndex: d.vlanParentIndex},
			// VLAN号
			VlanId: (int)(vlanId)}
		// 添加设备
		if err := netlink.LinkAdd(vlanIf); err != nil {
			return fmt.Errorf("Failed to add vlan device %s: %v", vlanIfName, err)
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	// 设置为UP状态
	if err := netlink.LinkSetUp(vlan); err != nil {
		return nil, fmt.Errorf("Failed to set up vlan device %s: %v", vlanIfName, err)
	}
	// 更新VLAN子接口索引号
	d.DeviceIndex = vlan.Attrs().Index
	return vlan, nil
}
func (d *VlanDriver) getVlanIfExist(vlanId uint16) (netlink.Link, error) {
	links, err := netlink.LinkList()
	if err != nil {
		return nil, err
	}
	for _, link := range links {
		// 遍历网络接口列表，查找VLAN类型的、VLAN ID匹配的、父接口匹配的
		// 隐含意味着对于每个VLAN，只需要一个子接口，所有连接到此VLAN的容器使用它
		if link.Type() == "vlan" {
			if vlan, ok := link.(*netlink.Vlan); !ok {
				return nil, fmt.Errorf("vlan device type case error: %T", link)
			} else {
				if vlan.VlanId == int(vlanId) &amp;&amp; vlan.ParentIndex == d.vlanParentIndex {
					return link, nil
				}
			}
		}
	}
	return nil, nil
}


// 通过IPVLAN两宿主机和容器连接起来的细节
// 配置网络沙盒
func configSboxDevice(result *t020.Result, args *skel.CmdArgs, sbox netlink.Link) error {
	// 配置MAC地址之前，应该将接口DOWN掉
	if err := netlink.LinkSetDown(sbox); err != nil {
		return fmt.Errorf("could not set link down for container interface %q: %v", sbox.Attrs().Name, err)
	}
	if sbox.Type() != "ipvlan" {
		// IPVlan不需要设置MAC地址，因为必须和父接口一致
		if err := netlink.LinkSetHardwareAddr(sbox, GenerateMACFromIP(result.IP4.IP.IP)); err != nil {
			return fmt.Errorf("could not set mac address for container interface %q: %v", sbox.Attrs().Name, err)
		}
	}
	// 获得容器网络命名空间
	netns, err := ns.GetNS(args.Netns)
	if err != nil {
		return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
	}
	// 总是要关闭命名空间
	defer netns.Close() // nolint: errcheck
	// 移动到网络命名空间中
	if err = netlink.LinkSetNsFd(sbox, int(netns.Fd())); err != nil {
		return fmt.Errorf("failed to move sbox device %q to netns: %v", sbox.Attrs().Name, err)
	}
	// 在网络命名空间中配置
	return netns.Do(func(_ ns.NetNS) error {
		// 改名，IPVLAN接口直接变为容器的eth0，就像VETH对的一端那样
		if err := netlink.LinkSetName(sbox, args.IfName); err != nil {
			return fmt.Errorf("failed to rename sbox device %q to %q: %v", sbox.Attrs().Name, args.IfName, err)
		}
		// 如果容器、宿主机上有多个网络设备，则需要禁用rp_filter（反向路径过滤）
		// 因为从宿主机来的ARP请求（广播报文），可能使用不确定的源IP，如果启用反向路径过滤，封包可能被丢弃
		if err := DisableRpFilter(args.IfName); err != nil {
			return fmt.Errorf("failed disable rp_filter to dev %s: %v", args.IfName, err)
		}
		// 禁用所有接口的rp_filter
		if err := DisableRpFilter("all"); err != nil {
			return fmt.Errorf("failed disable rp_filter to all: %v", err)
		}
		// 配置容器网络接口：将IP地址、路由添加到容器网络接口
		return cniutil.ConfigureIface(args.IfName, result)
	})
}
func ConfigureIface(ifName string, res *t020.Result) error {
	// 网络接口应该已经存在
	link, err := netlink.LinkByName(ifName)
	if err != nil {
		return fmt.Errorf("failed to lookup %q: %v", ifName, err)
	}
	// 设置为UP状态
	if err := netlink.LinkSetUp(link); err != nil {
		return fmt.Errorf("failed to set %q UP: %v", ifName, err)
	}

	// TODO(eyakubovich): IPv6
	addr := &amp;netlink.Addr{IPNet: &amp;res.IP4.IP, Label: ""}
	// 添加IP地址
	if err = netlink.AddrAdd(link, addr); err != nil {
		return fmt.Errorf("failed to add IP addr to %q: %v", ifName, err)
	}

	// 遍历并应用IPV4路由
	for _, r := range res.IP4.Routes {
		gw := r.GW
		if gw == nil {
			gw = res.IP4.Gateway
		}
		if err = ip.AddRoute(&amp;r.Dst, gw, link); err != nil {
			// we skip over duplicate routes as we assume the first one wins
			if !os.IsExist(err) {
				return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err)
			}
		}
	}
	return nil
}</pre>
<p>DEL命令比较简单：</p>
<ol>
<li>删除容器网络命名空间中所有VETH对</li>
<li>释放IP，就是调用IPAM插件，如果没有插件则什么都不做。Galaxy IPAM不会被调用，它总是自己负责IP地址的回收</li>
</ol>
<div class="blog_h3"><span class="graybg">galaxy-veth-host</span></div>
<p><a href="https://github.com/lyft/cni-ipvlan-vpc-k8s/blob/master/plugin/unnumbered-ptp/unnumbered-ptp.go">该插件</a>用于解决使用IPVlan L2模式下Pod访问不了宿主机IP、Service IP的问题。使用该插件，需要配置Galaxy：</p>
<pre class="crayon-plain-tag">{
  "NetworkConf":[
    {"name":"tke-route-eni","type":"tke-route-eni","eni":"eth1","routeTable":1},
    {"name":"galaxy-flannel","type":"galaxy-flannel", "delegate":{"type":"galaxy-veth"},"subnetFile":"/run/flannel/subnet.env"},
    {"name":"galaxy-k8s-vlan","type":"galaxy-k8s-vlan", "device":"eth0", "switch":"ipvlan", "ipvlan_mode":"l2", "mtu": 1500},
    {"name":"galaxy-k8s-sriov","type": "galaxy-k8s-sriov", "device": "eth0", "vf_num": 10},
    // 新增
    {
        "name":"galaxy-veth-host","type": "galaxy-veth-host", 
        // 这里配置K8S的服务CIDR         指明宿主机网卡
        "serviceCidr": "11.1.252.0/22", "hostInterface": "eth0", 
        // 容器中此插件创建的网卡名称     是否进行SNAT
        "containerInterface": "veth0", "ipMasq": true
    }
  ],
  "DefaultNetworks": ["galaxy-flannel"]
}</pre>
<p>Pod注解需要使用两个网络：</p>
<pre class="crayon-plain-tag">annotations:
  k8s.v1.cni.cncf.io/networks: "galaxy-k8s-vlan,galaxy-veth-host"</pre>
<p>代码解读： </p>
<pre class="crayon-plain-tag">package main

import (
	"encoding/json"
	"fmt"
	"math"
	"math/rand"
	"net"
	"os"
	"runtime"
	"sort"
	"strconv"
	"time"

	"github.com/containernetworking/cni/pkg/skel"
	"github.com/containernetworking/cni/pkg/types"
	"github.com/containernetworking/cni/pkg/types/current"
	"github.com/containernetworking/cni/pkg/version"
	"github.com/containernetworking/plugins/pkg/ip"
	"github.com/containernetworking/plugins/pkg/ns"
	"github.com/containernetworking/plugins/pkg/utils"
	"github.com/containernetworking/plugins/pkg/utils/sysctl"
	"github.com/coreos/go-iptables/iptables"
	"github.com/j-keck/arping"
	"github.com/vishvananda/netlink"
)

// constants for full jitter backoff in milliseconds, and for nodeport marks
const (
	maxSleep             = 10000 // 10.00s
	baseSleep            = 20    //  0.02
	RPFilterTemplate     = "net.ipv4.conf.%s.rp_filter"
	podRulePriority      = 1024
	nodePortRulePriority = 512
)

func init() {
	// this ensures that main runs only on main thread (thread group leader).
	// since namespace ops (unshare, setns) are done for a single thread, we
	// must ensure that the goroutine does not jump from OS thread to thread
	runtime.LockOSThread()
}

// PluginConf is whatever you expect your configuration json to be. This is whatever
// is passed in on stdin. Your plugin may wish to expose its functionality via
// runtime args, see CONVENTIONS.md in the CNI spec.
type PluginConf struct {
	types.NetConf

	// This is the previous result, when called in the context of a chained
	// plugin. Because this plugin supports multiple versions, we'll have to
	// parse this in two passes. If your plugin is not chained, this can be
	// removed (though you may wish to error if a non-chainable plugin is
	// chained.
	// If you need to modify the result before returning it, you will need
	// to actually convert it to a concrete versioned struct.
	RawPrevResult *map[string]interface{} `json:"prevResult"`
	PrevResult    *current.Result         `json:"-"`

	IPMasq             bool   `json:"ipMasq"`
	HostInterface      string `json:"hostInterface"`
	ServiceCidr        string `json:"serviceCidr"`
	ContainerInterface string `json:"containerInterface"`
	MTU                int    `json:"mtu"`
	TableStart         int    `json:"routeTableStart"`
	NodePortMark       int    `json:"nodePortMark"`
	NodePorts          string `json:"nodePorts"`
}

// parseConfig parses the supplied configuration (and prevResult) from stdin.
func parseConfig(stdin []byte) (*PluginConf, error) {
	conf := PluginConf{}

	if err := json.Unmarshal(stdin, &amp;conf); err != nil {
		return nil, fmt.Errorf("failed to parse network configuration: %v", err)
	}

	// Parse previous result.
	if conf.RawPrevResult != nil {
		resultBytes, err := json.Marshal(conf.RawPrevResult)
		if err != nil {
			return nil, fmt.Errorf("could not serialize prevResult: %v", err)
		}
		res, err := version.NewResult(conf.CNIVersion, resultBytes)
		if err != nil {
			return nil, fmt.Errorf("could not parse prevResult: %v", err)
		}
		conf.RawPrevResult = nil
		conf.PrevResult, err = current.NewResultFromResult(res)
		if err != nil {
			return nil, fmt.Errorf("could not convert result to current version: %v", err)
		}
	}
	// End previous result parsing

	if conf.HostInterface == "" {
		return nil, fmt.Errorf("hostInterface must be specified")
	}

	if conf.ContainerInterface == "" {
		return nil, fmt.Errorf("containerInterface must be specified")
	}

	if conf.NodePorts == "" {
		conf.NodePorts = "30000:32767"
	}

	if conf.NodePortMark == 0 {
		conf.NodePortMark = 0x2000
	}

	// start using tables by default at 256
	if conf.TableStart == 0 {
		conf.TableStart = 256
	}

	return &amp;conf, nil
}

func cmdAdd(args *skel.CmdArgs) error {
	conf, err := parseConfig(args.StdinData)
	if err != nil {
		return err
	}

	// 必须作为插件链调用，并且有前置插件
	if conf.PrevResult == nil {
		return fmt.Errorf("must be called as chained plugin")
	}

	// 从前序插件的结果中得到容器IP列表
	containerIPs := make([]net.IP, 0, len(conf.PrevResult.IPs))
	if conf.CNIVersion != "0.3.0" &amp;&amp; conf.CNIVersion != "0.3.1" {
		for _, ip := range conf.PrevResult.IPs {
			containerIPs = append(containerIPs, ip.Address.IP)
		}
	} else {
		for _, ip := range conf.PrevResult.IPs {
			if ip.Interface == nil {
				continue
			}
			intIdx := *ip.Interface
			if intIdx &gt;= 0 &amp;&amp; intIdx &lt; len(conf.PrevResult.Interfaces) &amp;&amp; conf.PrevResult.Interfaces[intIdx].Name != args.IfName {
				continue
			}
			containerIPs = append(containerIPs, ip.Address.IP)
		}
	}
	if len(containerIPs) == 0 {
		return fmt.Errorf("got no container IPs")
	}
	// 得到宿主机网络接口
	iface, err := netlink.LinkByName(conf.HostInterface)
	if err != nil {
		return fmt.Errorf("failed to lookup %q: %v", conf.HostInterface, err)
	}
	// 得到宿主机网络接口的地址列表
	hostAddrs, err := netlink.AddrList(iface, netlink.FAMILY_ALL)
	if err != nil || len(hostAddrs) == 0 {
		return fmt.Errorf("failed to get host IP addresses for %q: %v", iface, err)
	}

	netns, err := ns.GetNS(args.Netns)
	if err != nil {
		return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
	}
	defer netns.Close()

	containerIPV4 := false
	containerIPV6 := false
	for _, ipc := range containerIPs {
		if ipc.To4() != nil {
			containerIPV4 = true
		} else {
			containerIPV6 = true
		}
	}
	// 创建VETH对，并且在容器端进行配置：
	// 1. 如果启用了MASQ，则对出口是args.IfName的流量进行SNAT
	// 2. 为宿主机所有IP添加路由，走veth
	// 3. 为K8S服务IP添加旅游，则veth，网关设置为第一个宿主机IP
	hostInterface, _, err := setupContainerVeth(netns, conf.ServiceCidr, conf.ContainerInterface, conf.MTU,
		hostAddrs, conf.IPMasq, containerIPV4, containerIPV6, args.IfName, conf.PrevResult)
	if err != nil {
		return err
	}
	// 配置容器的宿主端：
	if err = setupHostVeth(hostInterface.Name, hostAddrs, conf.IPMasq, conf.TableStart, conf.PrevResult); err != nil {
		return err
	}

	if conf.IPMasq {
		// 在宿主端启用IP转发
		err := enableForwarding(containerIPV4, containerIPV6)
		if err != nil {
			return err
		}

		chain := utils.FormatChainName(conf.Name, args.ContainerID)
		comment := utils.FormatComment(conf.Name, args.ContainerID)
		for _, ipc := range containerIPs {
			addrBits := 128
			if ipc.To4() != nil {
				addrBits = 32
			}
			// 对来自容器IP的流量进行SNAT
			if err = ip.SetupIPMasq(&amp;net.IPNet{IP: ipc, Mask: net.CIDRMask(addrBits, addrBits)}, chain, comment); err != nil {
				return err
			}
		}
	}

	// 配置NodePort相关的iptables规则
	if err = setupNodePortRule(conf.HostInterface, conf.NodePorts, conf.NodePortMark); err != nil {
		return err
	}

	// Pass through the result for the next plugin
	return types.PrintResult(conf.PrevResult, conf.CNIVersion)
}

func setupContainerVeth(netns ns.NetNS, serviceCidr string, ifName string, mtu int,
	hostAddrs []netlink.Addr, masq, containerIPV4, containerIPV6 bool, k8sIfName string,
	pr *current.Result) (*current.Interface, *current.Interface, error) {
	hostInterface := &amp;current.Interface{}
	containerInterface := &amp;current.Interface{}
	// 在容器网络命名空间（netns）中执行
	err := netns.Do(func(hostNS ns.NetNS) error {
		// 创建VETH对，一端自动放入hostNS（这个变量netns.Do函数自动提供，为初始网络命名空间）
		hostVeth, contVeth0, err := ip.SetupVeth(ifName, mtu, hostNS)
		if err != nil {
			return err
		}
		hostInterface.Name = hostVeth.Name
		hostInterface.Mac = hostVeth.HardwareAddr.String()
		containerInterface.Name = contVeth0.Name
		// ip.SetupVeth函数不会获取VETH对的peer（第二个接口）的MAC地址，因此这里需要执行查询
		containerNetlinkIface, _ := netlink.LinkByName(contVeth0.Name)
		containerInterface.Mac = containerNetlinkIface.Attrs().HardwareAddr.String()
		containerInterface.Sandbox = netns.Path()

		// 本次CNI调用产生的两个网络接口，纳入到Result
		pr.Interfaces = append(pr.Interfaces, hostInterface, containerInterface)

		// 后面只是用到index属性，用上面Link对象不行么？
		contVeth, err := net.InterfaceByName(ifName)
		if err != nil {
			return fmt.Errorf("failed to look up %q: %v", ifName, err)
		}

		if masq {
			// 支持IPv4/IPv6的IP转发
			err := enableForwarding(containerIPV4, containerIPV6)
			if err != nil {
				return err
			}

			// 如果出口网卡是k8sIfName（容器主接口，通常是eth0，kubelet调用galaxy-sdn时提供）
			// ，则进行源地址转换
			err = setupSNAT(k8sIfName, "kube-proxy SNAT")
			if err != nil {
				return fmt.Errorf("failed to enable SNAT on %q: %v", k8sIfName, err)
			}
		}

		// 为宿主机的每个网络接口的地址添加路由条目
		for _, ipc := range hostAddrs {
			addrBits := 128
			if ipc.IP.To4() != nil {
				addrBits = 32
			}
			// 这些地址的出口网卡都设置为
			err := netlink.RouteAdd(&amp;netlink.Route{
				LinkIndex: contVeth.Index, // 新添加的VETH
				Scope:     netlink.SCOPE_LINK,
				Dst: &amp;net.IPNet{
					IP:   ipc.IP,
					Mask: net.CIDRMask(addrBits, addrBits),
				},
			})

			if err != nil {
				return fmt.Errorf("failed to add host route dst %v: %v", ipc.IP, err)
			}
		}

		_, serviceNet, err := net.ParseCIDR(serviceCidr)
		if err != nil {
			return fmt.Errorf("failed to parse service cidr :%v", err)
		}

		// 将K8S服务网段的路由设置为：出口网卡新添加的VETH，网关为宿主机第一个IP
		err = netlink.RouteAdd(&amp;netlink.Route{
			LinkIndex: contVeth.Index,
			Scope:     netlink.SCOPE_UNIVERSE,
			Dst:       serviceNet,
			// 封包发给宿主机第一个IP/网络接口处理。VETH的宿主端没有连接到什么网络接口
			// 这些网络接口可以作为下一跳，因为和VETH宿主端属于同一网络命名空间
			Gw: hostAddrs[0].IP,
		})
		if err != nil {
			return fmt.Errorf("failed to add service cidr route %v: %v", hostAddrs[0].IP, err)
		}

		// 为所有IPv4地址发送免费ARP
		for _, ipc := range pr.IPs {
			if ipc.Version == "4" {
				_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
			}
		}

		return nil
	})
	if err != nil {
		return nil, nil, err
	}
	return hostInterface, containerInterface, nil
}

func enableForwarding(ipv4 bool, ipv6 bool) error {
	if ipv4 {
		err := ip.EnableIP4Forward()
		if err != nil {
			return fmt.Errorf("Could not enable IPv6 forwarding: %v", err)
		}
	}
	if ipv6 {
		err := ip.EnableIP6Forward()
		if err != nil {
			return fmt.Errorf("Could not enable IPv6 forwarding: %v", err)
		}
	}
	return nil
}

func setupSNAT(ifName string, comment string) error {
	ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
	if err != nil {
		return fmt.Errorf("failed to locate iptables: %v", err)
	}
	rulespec := []string{"-o", ifName, "-j", "MASQUERADE"}
	//if ipt.HasRandomFully() {
	//	rulespec = append(rulespec, "--random-fully")
	//}
	rulespec = append(rulespec, "-m", "comment", "--comment", comment)
	return ipt.AppendUnique("nat", "POSTROUTING", rulespec...)
}

func setupHostVeth(vethName string, hostAddrs []netlink.Addr, masq bool, tableStart int, result *current.Result) error {
	// no IPs to route
	if len(result.IPs) == 0 {
		return nil
	}

	// lookup by name as interface ids might have changed
	veth, err := net.InterfaceByName(vethName)
	if err != nil {
		return fmt.Errorf("failed to lookup %q: %v", vethName, err)
	}

	// 对于所有容器IP，出口设置为VETH
	for _, ipc := range result.IPs {
		addrBits := 128
		if ipc.Address.IP.To4() != nil {
			addrBits = 32
		}

		err := netlink.RouteAdd(&amp;netlink.Route{
			LinkIndex: veth.Index,
			Scope:     netlink.SCOPE_LINK,
			Dst: &amp;net.IPNet{
				IP:   ipc.Address.IP,
				Mask: net.CIDRMask(addrBits, addrBits),
			},
		})

		if err != nil {
			return fmt.Errorf("failed to add host route dst %v: %v", ipc.Address.IP, err)
		}
	}

	// 为来自Pod的，目的地址是VPC的配置策略路由
	err = addPolicyRules(veth, result.IPs[0], result.Routes, tableStart)
	if err != nil {
		return fmt.Errorf("failed to add policy rules: %v", err)
	}

	// 为所有宿主机端的IP发送免费ARP
	for _, ipc := range hostAddrs {
		if ipc.IP.To4() != nil {
			_ = arping.GratuitousArpOverIface(ipc.IP, *veth)
		}
	}

	return nil
}

func addPolicyRules(veth *net.Interface, ipc *current.IPConfig, routes []*types.Route, tableStart int) error {
	table := -1

	// 对路由，来自先前插件调用的结果，进行排序
	sort.Slice(routes, func(i, j int) bool {
		return routes[i].Dst.String() &lt; routes[j].Dst.String()
	})

	// 尝试最多10次，向空表（table slot）写入路由
	for i := 0; i &lt; 10 &amp;&amp; table == -1; i++ {
		var err error
		// 寻找空白路由表
		table, err = findFreeTable(tableStart + rand.Intn(1000))
		if err != nil {
			return err
		}

		// 将所有路由添加到路由表
		for _, route := range routes {
			err := netlink.RouteAdd(&amp;netlink.Route{
				LinkIndex: veth.Index,     // 出口设置为宿主机VETH
				Dst:       &amp;route.Dst,     // 目标是先前CNI调用发现的路由，这些路由可能来自云提供商（VPC）
				Gw:        ipc.Address.IP, // 将Result的第一个IP作为网关
				Table:     table,          // 写在这个表中
			})
			if err != nil {
				table = -1
				break
			}
		}

		if table == -1 {
			// failed to add routes so sleep and try again on a different table
			wait := time.Duration(rand.Intn(int(math.Min(maxSleep,
				baseSleep*math.Pow(2, float64(i)))))) * time.Millisecond
			fmt.Fprintf(os.Stderr, "route table collision, retrying in %v\n", wait)
			time.Sleep(wait)
		}
	}

	// ensure we have a route table selected
	if table == -1 {
		return fmt.Errorf("failed to add routes to a free table")
	}

	// 创建路由策略
	rule := netlink.NewRule()
	// 如果流量来自VETH宿主端，也就是来自Pod
	rule.IifName = veth.Name
	rule.Table = table
	rule.Priority = podRulePriority

	err := netlink.RuleAdd(rule)
	if err != nil {
		return fmt.Errorf("failed to add policy rule %v: %v", rule, err)
	}

	return nil
}

func findFreeTable(start int) (int, error) {
	allocatedTableIDs := make(map[int]bool)
	// 遍历所有IPv4/IPv6的路由规则
	for _, family := range []int{netlink.FAMILY_V4, netlink.FAMILY_V6} {
		rules, err := netlink.RuleList(family)
		if err != nil {
			return -1, err
		}
		for _, rule := range rules {
			// 收集所有已经占用的table slot
			allocatedTableIDs[rule.Table] = true
		}
	}
	// 寻找第一个空白的slot
	for i := start; i &lt; math.MaxUint32; i++ {
		if !allocatedTableIDs[i] {
			return i, nil
		}
	}
	return -1, fmt.Errorf("failed to find free route table")
}

func setupNodePortRule(ifName string, nodePorts string, nodePortMark int) error {
	ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
	if err != nil {
		return fmt.Errorf("failed to locate iptables: %v", err)
	}

	// 确保NodePort流量被正确的标记
	if err := ipt.AppendUnique("mangle", "PREROUTING", "-i", ifName, "-p", "tcp",
		"--dport", nodePorts, "-j", "CONNMARK", "--set-mark", strconv.Itoa(nodePortMark),
		"-m", "comment", "--comment", "NodePort Mark"); err != nil {
		return err
	}
	if err := ipt.AppendUnique("mangle", "PREROUTING", "-i", ifName, "-p", "udp",
		"--dport", nodePorts, "-j", "CONNMARK", "--set-mark", strconv.Itoa(nodePortMark),
		"-m", "comment", "--comment", "NodePort Mark"); err != nil {
		return err
	}
	if err := ipt.AppendUnique("mangle", "PREROUTING", "-i", "veth+", "-j", "CONNMARK",
		"--restore-mark", "-m", "comment", "--comment", "NodePort Mark"); err != nil {
		return err
	}

	// 在宿主机网络接口上启用非严格的RP filter
	_, err = sysctl.Sysctl(fmt.Sprintf(RPFilterTemplate, ifName), "2")
	if err != nil {
		return fmt.Errorf("failed to set RP filter to loose for interface %q: %v", ifName, err)
	}

	// 对于标记为NodePort的流量，添加策略路由
	rule := netlink.NewRule()
	rule.Mark = nodePortMark
	rule.Table = 254 // main table
	rule.Priority = nodePortRulePriority

	exists := false
	rules, err := netlink.RuleList(netlink.FAMILY_V4)
	if err != nil {
		return fmt.Errorf("Unable to retrive IP rules %v", err)
	}

	for _, r := range rules {
		if r.Table == rule.Table &amp;&amp; r.Mark == rule.Mark &amp;&amp; r.Priority == rule.Priority {
			exists = true
			break
		}
	}
	if !exists {
		err := netlink.RuleAdd(rule)
		if err != nil {
			return fmt.Errorf("failed to add policy rule %v: %v", rule, err)
		}
	}

	return nil
}

// cmdDel is called for DELETE requests
func cmdDel(args *skel.CmdArgs) error {
	conf, err := parseConfig(args.StdinData)
	if err != nil {
		return err
	}

	if args.Netns == "" {
		return nil
	}

	// 网络命名空间存在，进行清理
	var ipnets []netlink.Addr
	vethPeerIndex := -1
	_ = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
		var err error

		if conf.IPMasq {
			iface, err := netlink.LinkByName(args.IfName)
			if err != nil {
				if err.Error() == "Link not found" {
					return ip.ErrLinkNotFound
				}
				return fmt.Errorf("failed to lookup %q: %v", args.IfName, err)
			}
			// 从args.IfName（通常是eth0）获取IP地址列表
			ipnets, err = netlink.AddrList(iface, netlink.FAMILY_ALL)
			if err != nil || len(ipnets) == 0 {
				return fmt.Errorf("failed to get IP addresses for %q: %v", args.IfName, err)
			}
		}

		vethIface, err := netlink.LinkByName(conf.ContainerInterface)
		if err != nil &amp;&amp; err != ip.ErrLinkNotFound {
			return err
		}
		vethPeerIndex, _ = netlink.VethPeerIndex(&amp;netlink.Veth{LinkAttrs: *vethIface.Attrs()})
		return nil
	})

	if conf.IPMasq {
		chain := utils.FormatChainName(conf.Name, args.ContainerID)
		comment := utils.FormatComment(conf.Name, args.ContainerID)
		for _, ipn := range ipnets {
			addrBits := 128
			if ipn.IP.To4() != nil {
				addrBits = 32
			}

			_ = ip.TeardownIPMasq(&amp;net.IPNet{IP: ipn.IP, Mask: net.CIDRMask(addrBits, addrBits)}, chain, comment)
		}

		if vethPeerIndex != -1 {
			link, err := netlink.LinkByIndex(vethPeerIndex)
			if err != nil {
				return nil
			}

			rule := netlink.NewRule()
			rule.IifName = link.Attrs().Name
			// ignore errors as we might be called multiple times
			_ = netlink.RuleDel(rule)
			_ = netlink.LinkDel(link)
		}
	}

	return nil
}

func main() {
	rand.Seed(time.Now().UnixNano())
	skel.PluginMain(cmdAdd, cmdDel, version.All)
}&amp;nbsp;</pre>
<div class="blog_h2"><span class="graybg">Galaxy守护进程</span></div>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>这个守护进程由DaemonSet galaxy提供，在所有K8S节点上运行，入口点如下：</p>
<pre class="crayon-plain-tag">package main

import (
	"math/rand"
	"time"

	"github.com/spf13/pflag"
	"k8s.io/component-base/cli/flag"
	"k8s.io/component-base/logs"
	glog "k8s.io/klog"
	"tkestack.io/galaxy/pkg/galaxy"
	"tkestack.io/galaxy/pkg/signal"
	"tkestack.io/galaxy/pkg/utils/ldflags"
)

func main() {
	// 随机化
	rand.Seed(time.Now().UTC().UnixNano())
	// 创建Galaxy结构
	galaxy := galaxy.NewGalaxy()
	galaxy.AddFlags(pflag.CommandLine)
	flag.InitFlags()
	logs.InitLogs()
	defer logs.FlushLogs()

	ldflags.PrintAndExitIfRequested()
	// 启动Galaxy
	if err := galaxy.Start(); err != nil {
		glog.Fatalf("Error start galaxy: %v", err)
	}
	// 等待信号，并停止Galaxy
	signal.BlockSignalHandler(func() {
		if err := galaxy.Stop(); err != nil {
			glog.Errorf("Error stop galaxy: %v", err)
		}
	})
}</pre>
<p>Galaxy结构的规格如下：</p>
<pre class="crayon-plain-tag">type Galaxy struct {
	// 对应Galaxy主配置文件
	JsonConf
	// 对应命令行选项
	*options.ServerRunOptions
	quitChan  chan struct{}
	dockerCli *docker.DockerInterface
	netConf   map[string]map[string]interface{}
	// 负责处理端口映射
	pmhandler *portmapping.PortMappingHandler
	client    kubernetes.Interface
	pm        *policy.PolicyManager
}

type JsonConf struct {
	NetworkConf     []map[string]interface{}
	DefaultNetworks []string
	ENIIPNetwork string
}</pre>
<p>初始化过程：</p>
<pre class="crayon-plain-tag">func NewGalaxy() *Galaxy {
	g := &amp;Galaxy{
		ServerRunOptions: options.NewServerRunOptions(),
		quitChan:         make(chan struct{}),
		netConf:          map[string]map[string]interface{}{},
	}
	return g
}

func (g *Galaxy) Init() error {
	if g.JsonConfigPath == "" {
		return fmt.Errorf("json config is required")
	}
	data, err := ioutil.ReadFile(g.JsonConfigPath)
	if err != nil {
		return fmt.Errorf("read json config: %v", err)
	}
	// 解析主配置文件
	if err := json.Unmarshal(data, &amp;g.JsonConf); err != nil {
		return fmt.Errorf("bad config %s: %v", string(data), err)
	}
	glog.Infof("Json Config: %s", string(data))
	// 配置合法性的简单校验
	if err := g.checkNetworkConf(); err != nil {
		return err
	}
	// 需要访问本机Docker
	dockerClient, err := docker.NewDockerInterface()
	if err != nil {
		return err
	}
	g.dockerCli = dockerClient
	g.pmhandler = portmapping.New("")
	return nil
}</pre>
<p>启动过程：</p>
<pre class="crayon-plain-tag">func (g *Galaxy) Start() error {
	// 初始化Galaxy配置
	if err := g.Init(); err != nil {
		return err
	}
	// 创建K8S客户端
	g.initk8sClient()
	// 启动Flannel垃圾回收器
	gc.NewFlannelGC(g.dockerCli, g.quitChan, g.cleanIPtables).Run()
	// 启用或禁用bridge-nf-call-iptables，此参数可以控制网桥转发的封包被不被iptables过滤
	kernel.BridgeNFCallIptables(g.quitChan, g.BridgeNFCallIptables)
	// 配置iptables转发
	kernel.IPForward(g.quitChan, g.IPForward)
	// 启动时，遍历节点上已有的Pod，对于需要进行端口映射的，执行端口映射
	if err := g.setupIPtables(); err != nil {
		return err
	}
	if g.NetworkPolicy {
		g.pm = policy.New(g.client, g.quitChan)
		go wait.Until(g.pm.Run, 3*time.Minute, g.quitChan)
	}
	if g.RouteENI {
		// 使用腾讯云ENI需要关闭反向路径过滤
		kernel.DisableRPFilter(g.quitChan)
		eni.SetupENIs(g.quitChan)
	}
	// 在UDS上监听、启动HTTP服务器、注册路由
	return g.StartServer()
}</pre>
<div class="blog_h3"><span class="graybg">CNI请求处理</span></div>
<p>上述代码结尾的g.StartServer()会调用下面的方法注册路由：</p>
<pre class="crayon-plain-tag">func (g *Galaxy) installHandlers() {
	ws := new(restful.WebService)
	ws.Route(ws.GET("/cni").To(g.cni))
	ws.Route(ws.POST("/cni").To(g.cni))
	restful.Add(ws)
}

// 处理CNI插件转发来的CNI请求
func (g *Galaxy) cni(r *restful.Request, w *restful.Response) {
	data, err := ioutil.ReadAll(r.Request.Body)
	if err != nil {
		glog.Warningf("bad request %v", err)
		http.Error(w, fmt.Sprintf("err read body %v", err), http.StatusBadRequest)
		return
	}
	defer r.Request.Body.Close() // nolint: errcheck
	// 将请求转换为CNIRequest，然后转换为PodRequest
	req, err := galaxyapi.CniRequestToPodRequest(data)
	if err != nil {
		glog.Warningf("bad request %v", err)
		http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
		return
	}
	req.Path = strings.TrimRight(fmt.Sprintf("%s:%s", req.Path, strings.Join(g.CNIPaths, ":")), ":")
	// 处理CNI请求
	result, err := g.requestFunc(req)
	if err != nil {
		http.Error(w, fmt.Sprintf("%v", err), http.StatusInternalServerError)
	} else {
		// Empty response JSON means success with no body
		w.Header().Set("Content-Type", "application/json")
		if _, err := w.Write(result); err != nil {
			glog.Warningf("Error writing %s HTTP response: %v", req.Command, err)
		}
	}
}</pre>
<p>当CNI插件galaxy-sdn调用Galaxy守护进程的/cni端点时，Galaxy会将请求从：</p>
<pre class="crayon-plain-tag">// Request sent to the Galaxy by the Galaxy SDN CNI plugin
type CNIRequest struct {
    // CNI environment variables, like CNI_COMMAND and CNI_NETNS
    Env map[string]string `json:"env,omitempty"`
    // CNI configuration passed via stdin to the CNI plugin
    Config []byte `json:"config,omitempty"`
}</pre>
<p>转换为PodRequest：</p>
<pre class="crayon-plain-tag">type PodRequest struct {
    // 需要执行的CNI命令
    Command string
    // 来自环境变量CNI_ARGS
    PodNamespace string
    // 来自环境变量CNI_ARGS
    PodName string
    // kubernetes pod ports
    Ports []k8s.Port
    // 存放操作结果的通道
    Result chan *PodResult
    // 通过环境变量传来的CNI_IFNAME、CNI_COMMAND、CNI_ARGS...
    *skel.CmdArgs
    // Galaxy需要委托其它CNI插件完成工作，这是传递给那些插件的参数
    //                 插件类型    参数名  参数值
    ExtendedCNIArgs map[string]map[string]json.RawMessage
}

// 请求处理结果
type PodResult struct {
    Response []byte
    Err error
}</pre>
<p>然后调用requestFunc方法处理转换后的CNI请求：</p>
<pre class="crayon-plain-tag">func (g *Galaxy) requestFunc(req *galaxyapi.PodRequest) (data []byte, err error) {
	start := time.Now()
	glog.Infof("%v, %s+", req, start.Format(time.StampMicro))
	// 处理ADD命令
	if req.Command == cniutil.COMMAND_ADD {
		defer func() {
			glog.Infof("%v, data %s, err %v, %s-", req, string(data), err, start.Format(time.StampMicro))
		}()
		var pod *corev1.Pod
		// 查找Pod
		pod, err = g.getPod(req.PodName, req.PodNamespace)
		if err != nil {
			return
		}
		result, err1 := g.cmdAdd(req, pod)
		if err1 != nil {
			err = err1
			return
		} else {
			// 转换结果格式
			result020, err2 := convertResult(result)
			if err2 != nil {
				err = err2
			} else {
				data, err = json.Marshal(result)
				if err != nil {
					return
				}
				// 如果处理成功，则执行端口映射，回顾一下启动的时候会对所有本节点现存的Pod进行端口映射
				err = g.setupPortMapping(req, req.ContainerID, result020, pod)
				if err != nil {
					g.cleanupPortMapping(req)
					return
				}
				pod.Status.PodIP = result020.IP4.IP.IP.String()
				// 处理网络策略
				if g.pm != nil {
					if err := g.pm.SyncPodChains(pod); err != nil {
						glog.Warning(err)
					}
					g.pm.SyncPodIPInIPSet(pod, true)
				}
			}
		}
	} else if req.Command == cniutil.COMMAND_DEL {
		defer glog.Infof("%v err %v, %s-", req, err, start.Format(time.StampMicro))
		err = cniutil.CmdDel(req.CmdArgs, -1)
		if err == nil {
			err = g.cleanupPortMapping(req)
		}
	} else {
		err = fmt.Errorf("unknown command %s", req.Command)
	}
	return
}</pre>
<p>requestFunc方法就是查询出Pod，将其作为参数的一部分，再调用 cmdAdd方法。需要注意，由于CNI版本很低，仅仅支持ADD/DEL两个命令。</p>
<p>ADD命令的处理逻辑如下：</p>
<pre class="crayon-plain-tag">func (g *Galaxy) cmdAdd(req *galaxyapi.PodRequest, pod *corev1.Pod) (types.Result, error) {
	// 通过解析Pod的注解、spec特殊字段，来构造出Pod的网络需求
	networkInfos, err := g.resolveNetworks(req, pod)
	if err != nil {
		return nil, err
	}
	return cniutil.CmdAdd(req.CmdArgs, networkInfos)
}

func CmdAdd(cmdArgs *skel.CmdArgs, networkInfos []*NetworkInfo) (types.Result, error) {
	// 每个NetworkInfo代表Pod需要加入的一个网络
	if len(networkInfos) == 0 {
		return nil, fmt.Errorf("No network info returned")
	}
	// 将网络需求的JSON格式保存在/var/lib/cni/galaxy/$ContainerID
	if err := saveNetworkInfo(cmdArgs.ContainerID, networkInfos); err != nil {
		return nil, fmt.Errorf("Error save network info %v for %s: %v", networkInfos, cmdArgs.ContainerID, err)
	}
	var (
		err    error
		// 前一个网络的结果
		result types.Result
	)
	// 处理并满足每一个网络需求
	for idx, networkInfo := range networkInfos {
		// 将来自k8s.v1.cni.galaxy.io/args注解的CNI参数添加到参数数组
		cmdArgs.Args = strings.TrimRight(fmt.Sprintf("%s;%s", cmdArgs.Args, BuildCNIArgs(networkInfo.Args)), ";")
		if result != nil {
			networkInfo.Conf["prevResult"] = result
		}
		// 委托给具体的CNI插件的ADD命令
		result, err = DelegateAdd(networkInfo.Conf, cmdArgs, networkInfo.IfName)
		if err != nil {
			// 如果失败，删除所有已经创建的CNI
			glog.Errorf("fail to add network %s: %v, begin to rollback and delete it", networkInfo.Args, err)
			// 从idx开始，倒过来依次为每种网络需求调用DEL命令
			delErr := CmdDel(cmdArgs, idx)
			glog.Warningf("fail to delete cni in rollback %v", delErr)
			return nil, fmt.Errorf("fail to establish network %s:%v", networkInfo.Args, err)
		}
	}
	if err != nil {
		return nil, err
	}
	return result, nil
}</pre>
<p>处理每一个网络需求时，会委托给相应的CNI插件：</p>
<pre class="crayon-plain-tag">// 每个插件的NetConf是在前面resolveNetworks时，从confdir=/etc/cni/net.d/读取出来的
func DelegateAdd(netconf map[string]interface{}, args *skel.CmdArgs, ifName string) (types.Result, error) {
	netconfBytes, err := json.Marshal(netconf)
	if err != nil {
		return nil, fmt.Errorf("error serializing delegate netconf: %v", err)
	}
	typ, err := getNetworkType(netconf)
	if err != nil {
		return nil, err
	}
	pluginPath, err := invoke.FindInPath(typ, strings.Split(args.Path, ":"))
	if err != nil {
		return nil, err
	}
	// 通过命令行调用
	glog.Infof("delegate add %s args %s conf %s", args.ContainerID, args.Args, string(netconfBytes))
	return invoke.ExecPluginWithResult(pluginPath, netconfBytes, &amp;invoke.Args{
		Command:       "ADD",
		ContainerID:   args.ContainerID,
		NetNS:         args.Netns,
		PluginArgsStr: args.Args,
		IfName:        ifName,
		Path:          args.Path,
	})
}</pre>
<div class="blog_h3"><span class="graybg">小结</span></div>
<p>这里做个小结：</p>
<ol>
<li>工作负载的网络需求（需要加入到哪些CNI网络），可以通过注解、Spec配置</li>
<li>Galaxy通过读取Pod注解、Spec，构造出网络列表</li>
<li>Galaxy会遍历CNI网络列表，依次通过命令行调用对应的CNI插件的ADD命令</li>
<li>如果遍历过程中出错，逆序的调用已经ADD的CNI插件的DEL命令</li>
</ol>
<p>第3、4其实就是新版本的CNI中NetworkList提供的能力。</p>
<div class="blog_h2"><span class="graybg">Galaxy IPAM</span></div>
<p>和Galaxy守护进程一样，Galaxy IPAM也是一个HTTP服务器。只是前者仅仅供本机的CNI插件galaxy-sdn调用，因此使用UDS，而Galaxy IPAM是供Scheduler调用，因此使用TCP。</p>
<div class="blog_h3"><span class="graybg">初始化</span></div>
<p>入口点如下：</p>
<pre class="crayon-plain-tag">func main() {
	// initialize rand seed
	rand.Seed(time.Now().UTC().UnixNano())

	s := server.NewServer()
	// add command line args
	s.AddFlags(pflag.CommandLine)

	flag.InitFlags()
	logs.InitLogs()
	defer logs.FlushLogs()

	// if checking version, print it and exit
	ldflags.PrintAndExitIfRequested()

	if err := s.Start(); err != nil {
		fmt.Fprintf(os.Stderr, "%v\n", err) // nolint: errcheck
		os.Exit(1)
	}
}</pre>
<p>启动Galaxy IPAM服务器： </p>
<pre class="crayon-plain-tag">func (s *Server) Start() error {
	if err := s.init(); err != nil {
		return fmt.Errorf("init server: %v", err)
	}
	// 启用InformerFactory监控K8S资源变化
	s.StartInformers(s.stopChan)
	// 多实例部署的Leader选举
	if s.LeaderElection.LeaderElect &amp;&amp; s.leaderElectionConfig != nil {
		leaderelection.RunOrDie(context.Background(), *s.leaderElectionConfig)
		return nil
	}
	return s.Run()
}

// Galaxy IPAM初始化
func (s *Server) init() error {
	// 读取配置文件
	if options.JsonConfigPath == "" {
		return fmt.Errorf("json config is required")
	}
	data, err := ioutil.ReadFile(options.JsonConfigPath)
	if err != nil {
		return fmt.Errorf("read json config: %v", err)
	}
	if err := json.Unmarshal(data, &amp;s.JsonConf); err != nil {
		return fmt.Errorf("bad config %s: %v", string(data), err)
	}
	// 初始化K8S客户端、IPAMContext
	s.initk8sClient()
	// 此浮动IP插件，实现了PodWatcher接口，能够监听Pod的生命周期事件
	s.plugin, err = schedulerplugin.NewFloatingIPPlugin(s.SchedulePluginConf, s.IPAMContext)
	if err != nil {
		return err
	}
	// 当有Pod事件后，调用浮动IP插件
	s.PodInformer.Informer().AddEventHandler(eventhandler.NewPodEventHandler(s.plugin))
	return nil
}

func (s *Server) Run() error {
	// 初始化浮动IP插件
	if err := s.plugin.Init(); err != nil {
		return err
	}
	// 运行浮动IP插件
	s.plugin.Run(s.stopChan)
	// 注册API路由
	go s.startAPIServer()
	// 注册供Scheduler调度的路由 /v1/filter、/v1/priority、/v1/bind
	s.startServer()
	return nil
}</pre>
<div class="blog_h3"><span class="graybg">FloatingIPPlugin</span></div>
<p>在上面的初始化流程中我们看到，在运行Galaxy IPAM服务器时，会初始化并运行s.plugin。类型为FloatingIPPlugin。</p>
<p>它是一个Pod事件监听器，实现接口：</p>
<pre class="crayon-plain-tag">type PodWatcher interface {
	AddPod(pod *corev1.Pod) error
	UpdatePod(oldPod, newPod *corev1.Pod) error
	DeletePod(pod *corev1.Pod) error
}</pre>
<p>当Pod事件发生后，上述方法会被调用。</p>
<p>它的初始化逻辑如下：</p>
<pre class="crayon-plain-tag">//                                  这个上下文包含各种客户端、Lister、Informer
func NewFloatingIPPlugin(conf Conf, ctx *context.IPAMContext) (*FloatingIPPlugin, error) {
	conf.validate()
	glog.Infof("floating ip config: %v", conf)
	plugin := &amp;FloatingIPPlugin{
		nodeSubnet:  make(map[string]*net.IPNet),
		IPAMContext: ctx,
		conf:        &amp;conf,
		unreleased:  make(chan *releaseEvent, 50000),
		// 这是一个哈希，每个key都可以被请求加锁
		dpLockPool:  keymutex.NewHashed(500000),
		podLockPool: keymutex.NewHashed(500000),
		crdKey:      NewCrdKey(ctx.ExtensionLister),
		crdCache:    crd.NewCrdCache(ctx.DynamicClient, ctx.ExtensionLister, 0),
	}
	// 初始化IPAM
	plugin.ipam = floatingip.NewCrdIPAM(ctx.GalaxyClient, plugin.FIPInformer)
	if conf.CloudProviderGRPCAddr != "" {
		plugin.cloudProvider = cloudprovider.NewGRPCCloudProvider(conf.CloudProviderGRPCAddr)
	}
	return plugin, nil
}

func (p *FloatingIPPlugin) Init() error {
	if len(p.conf.FloatingIPs) &gt; 0 {
		// 初始化IP池
		if err := p.ipam.ConfigurePool(p.conf.FloatingIPs); err != nil {
			return err
		}
	} else {
		// 配置文件中没有浮动IP，从ConfigMap中拉取
		glog.Infof("empty floatingips from config, fetching from configmap")
		if err := wait.PollInfinite(time.Second, func() (done bool, err error) {
			updated, err := p.updateConfigMap()
			if err != nil {
				glog.Warning(err)
			}
			return updated, nil
		}); err != nil {
			return fmt.Errorf("failed to get floatingip config from configmap: %v", err)
		}
	}
	glog.Infof("plugin init done")
	return nil
}</pre>
<p>它的run方法的逻辑，主要包括三部分：</p>
<pre class="crayon-plain-tag">func (p *FloatingIPPlugin) Run(stop chan struct{}) {
	if len(p.conf.FloatingIPs) == 0 {
		go wait.Until(func() {
// 1. 定期刷新ConfigMap floatingip-config，获取最新的浮动IP配置，并调用ipam.ConfigurePool配置IP池
			if _, err := p.updateConfigMap(); err != nil {
				glog.Warning(err)
			}
		}, time.Minute, stop)
	}
	go wait.Until(func() {
// 2. 定期同步Pod状态，必要的情况下进行IP释放
		if err := p.resyncPod(); err != nil {
			glog.Warningf("resync pod: %v", err)
		}
		p.syncPodIPsIntoDB()
	}, time.Duration(p.conf.ResyncInterval)*time.Minute, stop)
	for i := 0; i &lt; 5; i++ {
// 3. 从通道中读取IP释放事件，解除IP和Pod的绑定关系
		go p.loop(stop)
	}
}

func (p *FloatingIPPlugin) updateConfigMap() (bool, error) {
	// 拉取ConfigMap floatingip-config
	cm, err := p.Client.CoreV1().ConfigMaps(p.conf.ConfigMapNamespace).Get(p.conf.ConfigMapName, v1.GetOptions{})
	if err != nil {
		return false, fmt.Errorf("failed to get floatingip configmap %s_%s: %v", p.conf.ConfigMapName,
			p.conf.ConfigMapNamespace, err)
	}
	val, ok := cm.Data[p.conf.FloatingIPKey]
	if !ok {
		return false, fmt.Errorf("configmap %s_%s doesn't have a key floatingips", p.conf.ConfigMapName,
			p.conf.ConfigMapNamespace)
	}
	var updated bool
	// 调用IPAM，更新IP池
	if updated, err = p.ensureIPAMConf(&amp;p.lastIPConf, val); err != nil {
		return false, err
	}
	defer func() {
		if !updated {
			return
		}
		// 浮动IP配置更新，则节点允许的子网信息被清空
		p.nodeSubnetLock.Lock()
		defer p.nodeSubnetLock.Unlock()
		p.nodeSubnet = map[string]*net.IPNet{}
	}()
	return true, nil
}

// 释放以下Pod的IP
// 1. 所有者TAPP不存在的、已被删除的Pod
// 2. 所有者StatefulSet/Deployment存在、已被删除的Pod，但是没有配置IP为不变的
// 3. 所有者Deployment不需要这么多IP的、已被删除的的Pod
// 4. 所有者StatefulSet的副本数超过小于被删除Pod索引的
// 5. 未被删除，但是被驱逐的Pod
func (p *FloatingIPPlugin) resyncPod() error {
	glog.V(4).Infof("resync pods+")
	defer glog.V(4).Infof("resync pods-")
	resyncMeta := &amp;resyncMeta{}
	if err := p.fetchChecklist(resyncMeta); err != nil {
		return err
	}
	p.resyncAllocatedIPs(resyncMeta)
	return nil
}

// 拉取并处理待释放事件
func (p *FloatingIPPlugin) loop(stop chan struct{}) {
	for {
		select {
		case &lt;-stop:
			return
		case event := &lt;-p.unreleased:
			go func(event *releaseEvent) {
				// 解除Pod和IP地址的绑定关系
				if err := p.unbind(event.pod); err != nil {
					event.retryTimes++
					if event.retryTimes &gt; 3 {
						// leave it to resync to protect chan from explosion
						glog.Errorf("abort unbind for pod %s, retried %d times: %v", util.PodName(event.pod),
							event.retryTimes, err)
					} else {
						glog.Warningf("unbind pod %s failed for %d times: %v", util.PodName(event.pod),
							event.retryTimes, err)
						// backoff time if required
						time.Sleep(100 * time.Millisecond * time.Duration(event.retryTimes))
						p.unreleased &lt;- event
					}
				}
			}(event)
		}
	}
}</pre>
<p>当监听到Pod事件时，可能需要和IPAM同步Pod IP，或者产生一个待释放事件：</p>
<pre class="crayon-plain-tag">// AddPod does nothing
func (p *FloatingIPPlugin) AddPod(pod *corev1.Pod) error {
	return nil
}

// Pod更新时，和IPAM同步Pod IP
func (p *FloatingIPPlugin) UpdatePod(oldPod, newPod *corev1.Pod) error {
	if !p.hasResourceName(&amp;newPod.Spec) {
		return nil
	}
	// 先前的pod.Status.Phase不是终点状态（Failed/Succeeded），这一次是
	// 意味着Pod运行完毕，需要释放IP。通常是Job类Pod
	if !finished(oldPod) &amp;&amp; finished(newPod) {
		// Deployments will leave evicted pods
		// If it's a evicted one, release its ip
		glog.Infof("release ip from %s_%s, phase %s", newPod.Name, newPod.Namespace, string(newPod.Status.Phase))
		p.unreleased &lt;- &amp;releaseEvent{pod: newPod}
		return nil
	}
	// 同步Pod IP
	if err := p.syncPodIP(newPod); err != nil {
		glog.Warningf("failed to sync pod ip: %v", err)
	}
	return nil
}

func (p *FloatingIPPlugin) DeletePod(pod *corev1.Pod) error {
	if !p.hasResourceName(&amp;pod.Spec) {
		return nil
	}
	glog.Infof("handle pod delete event: %s_%s", pod.Name, pod.Namespace)
	// Pod删除后，产生一个待释放IP事件
	p.unreleased &lt;- &amp;releaseEvent{pod: pod}
	return nil
}</pre>
<p>和IPAM同步Pod IP的逻辑：</p>
<pre class="crayon-plain-tag">func (p *FloatingIPPlugin) syncPodIP(pod *corev1.Pod) error {
	// 只有到达Running状态的，才同步
	if pod.Status.Phase != corev1.PodRunning {
		return nil
	}
	// 同步干的事情是，如果Pod就有ipinfos注解，并且IP地址没有在池中分配，那么在池中将IP分配给Pod
	if pod.Annotations == nil {
		return nil
	}
	// 这个有点意思，lockpod会立即调用，并且返回一个unlock函数，此函数会在syncPodIP退出时执行。代码简洁
	// 防止有其它线程再操作此Pod
	defer p.lockPod(pod.Name, pod.Namespace)()
	keyObj, err := util.FormatKey(pod)
	if err != nil {
		glog.V(5).Infof("sync pod %s/%s ip formatKey with error %v", pod.Namespace, pod.Name, err)
		return nil
	}
	cniArgs, err := constant.UnmarshalCniArgs(pod.Annotations[constant.ExtendedCNIArgsAnnotation])
	if err != nil {
		return err
	}
	ipInfos := cniArgs.Common.IPInfos
	for i := range ipInfos {
		if ipInfos[i].IP == nil || ipInfos[i].IP.IP == nil {
			continue
		}
		// 遍历所有注解中的IP地址，调用IPAM的AllocateSpecificIP方法分配IP
		if err := p.syncIP(keyObj.KeyInDB, ipInfos[i].IP.IP, pod); err != nil {
			glog.Warningf("sync pod %s ip %s: %v", keyObj.KeyInDB, ipInfos[i].IP.IP.String(), err)
		}
	}
	return nil
}</pre>
<p>小结一下FloatingIPPlugin的职责。它主要负责监控ConfigMap、Pod的变化。当ConfigMap变化后更新IPAM的配置，当Pod发生变化时，更新IPAM池中的IP分配信息。</p>
<p>此外FloatingIPPlugin还提供了调度的核心算法，包括Filter、Prioritize、Bind这几个方法，我们在后面在探讨。</p>
<div class="blog_h3"><span class="graybg">IPAM</span></div>
<p>FloatingIPPlugin.ipam字段，代表了IP地址分配管理的核心，它的接口：</p>
<pre class="crayon-plain-tag">type IPAM interface {
	// 初始化IP池
	ConfigurePool([]*FloatingIPPool) error
	// 释放映射中键匹配的IP，返回已分配、未分配IP地址的映射
	ReleaseIPs(map[string]string) (map[string]string, map[string]string, error)
	// 为Pod分配指定的IP
	AllocateSpecificIP(string, net.IP, Attr) error
	// 在子网中分配IP
	AllocateInSubnet(string, *net.IPNet, Attr) (net.IP, error)
	// 在给定的多个IP范围中，都分配一个IP
	AllocateInSubnetsAndIPRange(string, *net.IPNet, [][]nets.IPRange, Attr) ([]net.IP, error)
	// 在指定的子网中，以指定的key分配IP
	AllocateInSubnetWithKey(oldK, newK, subnet string, attr Attr) error
	// 保留IP
	ReserveIP(oldK, newK string, attr Attr) (bool, error)
	// 释放IP
	Release(string, net.IP) error
	// 根据Key返回的一个匹配
	First(string) (*FloatingIPInfo, error)
	// 将IP地址转换为浮动IP对象
	ByIP(net.IP) (FloatingIP, error)
	// 根据前缀查询
	ByPrefix(string) ([]*FloatingIPInfo, error)
	// 根据关键字查询
	ByKeyword(string) ([]FloatingIP, error)
	// 返回节点的子网信息
	NodeSubnet(net.IP) *net.IPNet
} </pre>
<div class="blog_h3"><span class="graybg">调度器扩展</span></div>
<p>Galaxy IPAM对接K8S调度器，使用的是Scheduler Extender Webhook，也就是它提供若干HTTP接口供调度器使用。</p>
<p>这些接口实现为Galaxy IPAM Server的方法，在startServer()的时候注册：</p>
<pre class="crayon-plain-tag">// 筛选匹配的节点
func (s *Server) filter(request *restful.Request, response *restful.Response) {
	// K8S标准API，ExtenderArgs包含了当前正被调度的Pod，以及可用的（已经被筛选过的）节点列表
	args := new(schedulerapi.ExtenderArgs)
	if err := request.ReadEntity(&amp;args); err != nil {
		glog.Error(err)
		_ = response.WriteError(http.StatusInternalServerError, err)
		return
	}
	glog.V(5).Infof("POST filter %v", *args)
	start := time.Now()
	glog.V(3).Infof("filtering %s_%s, start at %d+", args.Pod.Name, args.Pod.Namespace, start.UnixNano())
	// 调用FloatingIPPlugin
	filteredNodes, failedNodesMap, err := s.plugin.Filter(&amp;args.Pod, args.Nodes.Items)
	glog.V(3).Infof("filtering %s_%s, start at %d-", args.Pod.Name, args.Pod.Namespace, start.UnixNano())
	args.Nodes.Items = filteredNodes
	errStr := ""
	if err != nil {
		errStr = err.Error()
	}
	_ = response.WriteEntity(schedulerapi.ExtenderFilterResult{
		// 可以调度的节点
		Nodes:       args.Nodes,
		// 不可调度的节点，以及不可调度的原因
		FailedNodes: failedNodesMap,
		// 错误信息
		Error:       errStr,
	})
}

// 节点优先级判定（评分）
func (s *Server) priority(request *restful.Request, response *restful.Response) {
	args := new(schedulerapi.ExtenderArgs)
	if err := request.ReadEntity(&amp;args); err != nil {
		glog.Error(err)
		_ = response.WriteError(http.StatusInternalServerError, err)
		return
	}
	glog.V(5).Infof("POST priority %v", *args)
	// 调用FloatingIPPlugin
	hostPriorityList, err := s.plugin.Prioritize(&amp;args.Pod, args.Nodes.Items)
	if err != nil {
		glog.Warningf("prioritize err: %v", err)
	}
	_ = response.WriteEntity(*hostPriorityList)
}


// 绑定Pod到节点
func (s *Server) bind(request *restful.Request, response *restful.Response) {
	args := new(schedulerapi.ExtenderBindingArgs)
	if err := request.ReadEntity(&amp;args); err != nil {
		glog.Error(err)
		_ = response.WriteError(http.StatusInternalServerError, err)
		return
	}
	glog.V(5).Infof("POST bind %v", *args)
	start := time.Now()
	glog.V(3).Infof("binding %s_%s to %s, start at %d+", args.PodName, args.PodNamespace, args.Node, start.UnixNano())
	// 调用FloatingIPPlugin
	err := s.plugin.Bind(args)
	glog.V(3).Infof("binding %s_%s to %s, start at %d-", args.PodName, args.PodNamespace, args.Node, start.UnixNano())
	var result schedulerapi.ExtenderBindingResult
	if err != nil {
		glog.Warningf("bind err: %v", err)
		result.Error = err.Error()
	}
	_ = response.WriteEntity(result)
}</pre>
<p>这些方法仅仅是负责HTTP相关处理，核心是上文我们提到的FloatingIPPlugin的几个方法：</p>
<pre class="crayon-plain-tag">// 没有可用IP的节点被标记为failedNodes
// 如果Pod不需要浮动IP，则failedNodes为空
func (p *FloatingIPPlugin) Filter(pod *corev1.Pod, nodes []corev1.Node) (
    []corev1.Node, schedulerapi.FailedNodesMap, error) {
	start := time.Now()
	failedNodesMap := schedulerapi.FailedNodesMap{}
	// 没有指定自定义资源tke.cloud.tencent.com/eni-ip的request，认为不需要浮动IP
	if !p.hasResourceName(&amp;pod.Spec) {
		return nodes, failedNodesMap, nil
	}
	filteredNodes := []corev1.Node{}
	// 开始过滤，期间锁定针对此Pod的操作
	defer p.lockPod(pod.Name, pod.Namespace)()
	// 读取注解k8s.v1.cni.galaxy.io/args，获取Pod要加入的子网信息
	subnetSet, err := p.getSubnet(pod)
	if err != nil {
		return filteredNodes, failedNodesMap, err
	}
	// 遍历所有节点
	for i := range nodes {
		nodeName := nodes[i].Name
		// 得到节点所属的子网
		subnet, err := p.getNodeSubnet(&amp;nodes[i])
		if err != nil {
			failedNodesMap[nodes[i].Name] = err.Error()
			continue
		}
		// 如果节点可以分配Pod要加入的子网，则保留节点，否则，丢弃节点
		if subnetSet.Has(subnet.String()) {
			filteredNodes = append(filteredNodes, nodes[i])
		} else {
			failedNodesMap[nodeName] = "FloatingIPPlugin:NoFIPLeft"
		}
	}
	if glog.V(5) {
		nodeNames := make([]string, len(filteredNodes))
		for i := range filteredNodes {
			nodeNames[i] = filteredNodes[i].Name
		}
		glog.V(5).Infof("filtered nodes %v failed nodes %v for %s_%s", nodeNames, failedNodesMap,
			pod.Namespace, pod.Name)
	}
	metrics.ScheduleLatency.WithLabelValues("filter").Observe(time.Since(start).Seconds())
	return filteredNodes, failedNodesMap, nil
}

// 打分目前是空实现
func (p *FloatingIPPlugin) Prioritize(pod *corev1.Pod, nodes []corev1.Node) (*schedulerapi.HostPriorityList, error) {
	list := &amp;schedulerapi.HostPriorityList{}
	if !p.hasResourceName(&amp;pod.Spec) {
		return list, nil
	}
	//TODO
	return list, nil
}

// 将新的浮动IP绑定给Pod，或者重用以有的IP
func (p *FloatingIPPlugin) Bind(args *schedulerapi.ExtenderBindingArgs) error {
	start := time.Now()
	pod, err := p.PodLister.Pods(args.PodNamespace).Get(args.PodName)
	if err != nil {
		return fmt.Errorf("failed to find pod %s: %w", util.Join(args.PodName, args.PodNamespace), err)
	}
	if !p.hasResourceName(&amp;pod.Spec) {
		return fmt.Errorf("pod which doesn't want floatingip have been sent to plugin")
	}
	defer p.lockPod(pod.Name, pod.Namespace)()
	// 为Pod生成键，键由要加入的池、Pod命名空间、Pod名字、所属StatefulSet/Deployment的名字构成
	keyObj, err := util.FormatKey(pod)
	if err != nil {
		return err
	}
	// 分配IP给Pod
	cniArgs, err := p.allocateIP(keyObj.KeyInDB, args.Node, pod)
	if err != nil {
		return err
	}
	data, err := json.Marshal(cniArgs)
	if err != nil {
		return fmt.Errorf("marshal cni args %v: %v", *cniArgs, err)
	}
	// 添加k8s.v1.cni.galaxy.io/args注解
	bindAnnotation := map[string]string{constant.ExtendedCNIArgsAnnotation: string(data)}
	var err1 error
	if err := wait.PollImmediate(time.Millisecond*500, 3*time.Second, func() (bool, error) {
		// 执行绑定操作，为Pod添加binding子资源
		if err := p.Client.CoreV1().Pods(args.PodNamespace).Bind(&amp;corev1.Binding{
			ObjectMeta: v1.ObjectMeta{Namespace: args.PodNamespace, Name: args.PodName, UID: args.PodUID,
				Annotations: bindAnnotation},
			Target: corev1.ObjectReference{
				Kind: "Node",
				Name: args.Node,
			},
		}); err != nil {
			err1 = err
			if apierrors.IsNotFound(err) {
				// Pod已经不存在了，终止轮询
				return false, err
			}
			return false, nil
		}
		glog.Infof("bind pod %s to %s with %s", keyObj.KeyInDB, args.Node, string(data))
		return true, nil
	}); err != nil {
		if apierrors.IsNotFound(err1) {
			// 绑定过程中发现Pod消失，说明有人将其删除了。已经分配的IP需要回收
			glog.Infof("binding returns not found for pod %s, putting it into unreleased chan", keyObj.KeyInDB)
			// attach ip annotation
			p.unreleased &lt;- &amp;releaseEvent{pod: pod}
		}
		// If fails to update, depending on resync to update
		return fmt.Errorf("update pod %s: %w", keyObj.KeyInDB, err1)
	}
	metrics.ScheduleLatency.WithLabelValues("bind").Observe(time.Since(start).Seconds())
	return nil
}</pre>
<p>我们再来看一下在Bind阶段，分配IP的细节：</p>
<pre class="crayon-plain-tag">func (p *FloatingIPPlugin) allocateIP(key string, nodeName string, pod *corev1.Pod) (*constant.CniArgs, error) {
	cniArgs, err := getPodCniArgs(pod)
	if err != nil {
		return nil, err
	}
	ipranges := cniArgs.RequestIPRange</pre>
<p>getPodCniArgs读取k8s.v1.cni.galaxy.io/args注解为结构： </p>
<pre class="crayon-plain-tag">type CniArgs struct {
	// 用户可以指定从什么地址范围分配IP
	RequestIPRange [][]nets.IPRange `json:"request_ip_range,omitempty"`
	// 这些参数最终会传递给底层CNI插件，作为key1=val1;key2=val2格式的参数
	Common CommonCniArgs `json:"common"`
}</pre>
<p>allocateIP会使用key去查询已经分配给key的、每个iprange中的IP信息：</p>
<pre class="crayon-plain-tag">ipInfos, err := p.ipam.ByKeyAndIPRanges(key, ipranges)
	if err != nil {
		return nil, fmt.Errorf("failed to query floating ip by key %s: %v", key, err)
	}
	if len(ipranges) == 0 &amp;&amp; len(ipInfos) &gt; 0 {
		// reuse only one if requesting only one ip
		ipInfos = ipInfos[:1]
	}
	// 那些尚未分配IP地址的ipranges
	var unallocatedIPRange [][]nets.IPRange // those does not have allocated ips
	reservedIPs := sets.NewString()
	for i := range ipInfos {
		if ipInfos[i] == nil {
			// 未分配
			unallocatedIPRange = append(unallocatedIPRange, ipranges[i])
		} else {
			// 已经分配给当前key，需要保留
			reservedIPs.Insert(ipInfos[i].IP.String())
		}
	}
	policy := parseReleasePolicy(&amp;pod.ObjectMeta)
	attr := floatingip.Attr{Policy: policy, NodeName: nodeName, Uid: string(pod.UID)}</pre>
<p>检查从IPAM获取的ipinfos，如果PodUid和当前Pod.GetUID()，说明可能先前版本的Pod尚未删除，等待删除（bind函数返回错误）：</p>
<pre class="crayon-plain-tag">for _, ipInfo := range ipInfos {
		// check if uid missmatch, if we delete a statfulset/tapp and creates a same name statfulset/tapp immediately,
		// galaxy-ipam may receive bind event for new pod early than deleting event for old pod
		if ipInfo != nil &amp;&amp; ipInfo.PodUid != "" &amp;&amp; ipInfo.PodUid != string(pod.GetUID()) {
			return nil, fmt.Errorf("waiting for delete event of %s before reuse this ip", key)
		}
	}</pre>
<p>如果有需要新分配的IP，调用IPAM进行分配：</p>
<pre class="crayon-plain-tag">if len(unallocatedIPRange) &gt; 0 || len(ipInfos) == 0 {
		// 节点所属的子网
		subnet, err := p.queryNodeSubnet(nodeName)
		if err != nil {
			return nil, err
		}
		// 执行分配
		if _, err := p.ipam.AllocateInSubnetsAndIPRange(key, subnet, unallocatedIPRange, attr); err != nil {
			return nil, err
		}
		// 更新IP信息
		ipInfos, err = p.ipam.ByKeyAndIPRanges(key, ipranges)
		if err != nil {
			return nil, fmt.Errorf("failed to query floating ip by key %s: %v", key, err)
		}
	}</pre>
<p>检查已分配IP重用的情况，并更新IPAM属性： </p>
<pre class="crayon-plain-tag">for _, ipInfo := range ipInfos {
		//...
		if reservedIPs.Has(ipInfo.IP.String()) {
			glog.Infof("%s reused %s, updating attr to %v", key, ipInfo.IPInfo.IP.String(), attr)
			if err := p.ipam.UpdateAttr(key, ipInfo.IPInfo.IP.IP, attr); err != nil {
				return nil, fmt.Errorf("failed to update floating ip release policy: %v", err)
			}
		}
	}</pre>
<p>回填Pod注解：</p>
<pre class="crayon-plain-tag">// 新分配的IP地址
	var allocatedIPs []string
	// 所有IP地址
	var ret []constant.IPInfo
	for _, ipInfo := range ipInfos {
		if !reservedIPs.Has(ipInfo.IP.String()) {
			allocatedIPs = append(allocatedIPs, ipInfo.IP.String())
		}
		ret = append(ret, ipInfo.IPInfo)
	}
	glog.Infof("%s reused ips %v, allocated ips %v, attr %v", key, reservedIPs.List(), allocatedIPs, attr)
	// 回填
	cniArgs.Common.IPInfos = ret
	return &amp;cniArgs, nil</pre>
<div class="blog_h3"><span class="graybg">小结</span></div>
<p>当一个Pod创建时，如果它的Spec上具有自定义的资源请求tke.cloud.tencent.com/eni-ip，则调度器认为，需要为它分配浮动IP。</p>
<p>由于在floatingip-config中，可以配置多组浮动IP子网。Pod应该通过注解：</p>
<ol>
<li>k8s.v1.cni.galaxy.io/args 来声明自己需要加入什么子网、需要请求什么范围的IP地址</li>
<li>tke.cloud.tencent.com/eni-ip-pool 来声明需要使用哪个IP池</li>
</ol>
<p>Pod创建后，首先进入调度阶段。作为K8S调度器扩展的Galaxy IPAM会按需进行IP分配，并将结果写入到Pod的注解中。</p>
<p>当Kubelet在本地启动Pod时， 会调用CNI插件galaxy-sdn，galaxy-sdn则会调用Galaxy守护进程。后者则<span style="background-color: #c0c0c0;">通过命令行调用galaxy-k8s-vlan等底层CNI插件，并且将Pod注解k8s.v1.cni.galaxy.io/args中的键值转换为key1=val1;key2=val2CNI参数传递</span>。</p>
<p>galaxy-k8s-vlan插件会调用ipam.Allocate，获取CNI参数中的IP地址信息，并转换为v0.20格式的Result，然后调用setupNetwork将IP地址设置到容器的网络接口上。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/galaxy-study-note">Galaxy学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/galaxy-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Cilium学习笔记</title>
		<link>https://blog.gmem.cc/cilium</link>
		<comments>https://blog.gmem.cc/cilium#comments</comments>
		<pubDate>Mon, 22 Jun 2020 10:56:30 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[CNI]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=37551</guid>
		<description><![CDATA[<p>简介 Cilium Cilium是在Docker/K8S之类的容器管理平台下，透明的为应用程序服务提供安全网络连接的开源软件。Cilium的底层技术是eBPF，eBPF完全在内核中运行，因此改变Cilium的安全策略时不需要程序代码、容器配置的任何变更。 Hubble Hubble是一个完全分布式的网络和安全可观察性平台。它构建在Cilium + eBPF之上，它以完全透明的方式，实现了服务、网络基础设施的通信/行为的深度可观察性。 由于可观察性依赖于eBPF，因此是可动态编程的、成本最小化的、可深度定制的。 Hubble可以回答以下问题： 服务依赖和通信关系图： 两个服务是否通信，通信频度如何，服务之间的依赖关系是怎样的？ 进行了哪些HTTP调用？ 服务消费了哪些Kafka主题，发布了哪些Kafka主题 网络监控和报警： 网络通信是否失败，为何失败？是DNS导致的失败？还是L4/L7的原因 最近5分钟有哪些服务存在DNS解析问题 哪些服务出现连接超时、中断的问题 无应答SYN请求的频率是多高 应用程序监控： 特定服务/或者整个集群的5xx/4xx HTTP响应的频率是多高 HTTP请求延迟的95th/99th位数是多少 <a class="read-more" href="https://blog.gmem.cc/cilium">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/cilium">Cilium学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<div class="blog_h2"><span class="graybg">Cilium</span></div>
<p>Cilium是在Docker/K8S之类的容器管理平台下，透明的为应用程序服务提供安全网络连接的开源软件。Cilium的底层技术是eBPF，eBPF完全在内核中运行，因此改变Cilium的安全策略时不需要程序代码、容器配置的任何变更。</p>
<div class="blog_h2"><span class="graybg">Hubble</span></div>
<p>Hubble是一个完全分布式的网络和安全可观察性平台。它构建在Cilium + eBPF之上，它以完全透明的方式，实现了服务、网络基础设施的通信/行为的深度可观察性。</p>
<p>由于可观察性依赖于eBPF，因此是可动态编程的、成本最小化的、可深度定制的。</p>
<p>Hubble可以回答以下问题：</p>
<ol>
<li>服务依赖和通信关系图：
<ol>
<li>两个服务是否通信，通信频度如何，服务之间的依赖关系是怎样的？</li>
<li>进行了哪些HTTP调用？</li>
<li>服务消费了哪些Kafka主题，发布了哪些Kafka主题</li>
</ol>
</li>
<li>网络监控和报警：
<ol>
<li>网络通信是否失败，为何失败？是DNS导致的失败？还是L4/L7的原因</li>
<li>最近5分钟有哪些服务存在DNS解析问题</li>
<li>哪些服务出现连接超时、中断的问题</li>
<li>无应答SYN请求的频率是多高</li>
</ol>
</li>
<li>应用程序监控：
<ol>
<li>特定服务/或者整个集群的5xx/4xx HTTP响应的频率是多高</li>
<li>HTTP请求延迟的95th/99th位数是多少</li>
<li>哪两个服务之间的延迟最高</li>
</ol>
</li>
<li>安全可观察性：
<ol>
<li>哪些服务因为网络策略而出现连接被阻止</li>
<li>哪些服务被从集群外部访问</li>
<li>哪些服务尝试解析了特定的域名</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">优势</span></div>
<p>在监控（系统和应用）领域，从来没有一个技术能像eBPF一样做到如此的高性能、细粒度、透明化，以及动态性。</p>
<p>现代数据中心中运行的应用程序，通常基于微服务架构设计，应用程序被拆分为大量独立的小服务，这些服务基于轻量级的协议（例如HTTP）进行通信。这些微服务通常容器化部署，可以动态按需创建、销毁、扩缩容。</p>
<p>这种容器化的微服务架构，在连接安全性方面引入了挑战。传统的Linux网络安全机制，例如iptables，基于IP地址、TCP/UDP端口进行过滤。在容器化架构下IP地址会很快变化，这会导致ACL规则、LB表需要不断的、加速（随着业务规模扩大）更新。由于IP地址不稳定，给实现精准可观察性也带来了挑战。</p>
<p>依赖于eBPF，Cilium能够基于服务/Pod/容器的标识（而非IP地址），实现安全策略的更新。能够在L7/L4/L3进行过滤。</p>
<div class="blog_h2"><span class="graybg">能力</span></div>
<div class="blog_h3"><span class="graybg">透明的保护API</span></div>
<p>能够在L7进行过滤，支持REST/HTTP、gRPC、Kafka等协议。从而实现：</p>
<ol>
<li>允许对/public/.*的GET请求，禁止其它任何请求</li>
<li>允许service1在Kafka的主题topic1上发布消息，service2在topic1上消费消息，禁止其它Kafka消息</li>
<li>要求所有HTTP请求具有头X-Token: [0-9]+</li>
</ol>
<div class="blog_h3"><span class="graybg">基于身份标识的安全访问</span></div>
<p>经典的容器防火墙，基于IP地址/端口来进行封包过滤，每当有新的容器启动，都要求所有服务器更新防火墙规则。</p>
<p>Cilium支持为一组应用程序分配身份标帜，共享同一安全策略。身份标识将关联到容器发出的所有网络封包，在接收封包的节点，可以校验身份信息。</p>
<div class="blog_h3"><span class="graybg">外部服务安全访问</span></div>
<p>上一段提到了，Cilium支持基于身份标识的内部服务之间的安全访问机制。对于外部服务，Cilium支持经典的基于CIDR的ingress/egress安全策略。</p>
<div class="blog_h3"><span class="graybg">简单的容器网络</span></div>
<p>Cilium支持一个简单的、扁平的L3网络，能够跨越多个集群，连接所有容器。通过使用host scope的IP分配器，IP分配被保持简单，每个主机可以独立进行分配分配，不需要相互协作。</p>
<p>支持以下多节点网络模型：</p>
<ol>
<li>Overlay：目前内置支持VxLAN和Geneve，所有Linux支持的封装格式都可以启用</li>
<li>Native Routing：也叫Direct Routing，使用Linux宿主机的路由表，底层网络必须具有路由容器IP的能力。支持原生的IP6网络，能够和云网络路由器协作</li>
</ol>
<div class="blog_h3"><span class="graybg">负载均衡</span></div>
<p>Cilium实现了分布式的负载均衡，可以完全代替kube-proxy。LB基于eBPF实现，使用高效的、可无限扩容的哈希表来存储信息。</p>
<p>对于南北向负载均衡，Cilium作了最大化性能的优化。支持XDP、DSR（Direct Server Return，LB仅仅修改转发封包的目标MAC地址）。</p>
<p>对于东西向负载均衡，Cilium在内核套接字层（TCP连接时）执行高效的service-to-backend转换（通过eBPF直接修改封包），避免了更低层次（IP）中per-packet的NAT（依赖conntrack，在高并发或大量连接的情况下有若干问题）操作成本。</p>
<div class="blog_h3"><span class="graybg">带宽管理</span></div>
<p>Cilium利用eBPF实现高效的基于EDT（Earliest Departure Time）的egress限速，能够很大程度上避免HTB/TBF等经典qdisc的缺点，包括传输尾延迟，多队列NIC下的锁问题。</p>
<div class="blog_h3"><span class="graybg">监控和诊断</span></div>
<p>对于任何分布式系统，可观察性对于监控和故障诊断都非常重要。Cilium提供了更好的诊断工具：</p>
<ol>
<li>携带元数据的事件监控：当封包被丢弃时，不但报告源地址，而能提供 完整的发送者/接收者元数据</li>
<li>策略决策跟踪：支持跟踪并发现是什么策略导致封包丢弃或请求拒绝</li>
<li>支持通过Prometheus暴露指标</li>
<li>Hubble：一个专门为Cilium设计的可观察性平台，能够提供服务依赖图、监控和报警</li>
</ol>
<div class="blog_h1"><span class="graybg">架构</span></div>
<div class="blog_h2"><span class="graybg">整体架构</span></div>
<p>整体上的组件架构如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium-arch.png"><img class="size-large wp-image-37593 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium-arch-1024x912.png" alt="cilium-arch" width="710" height="632" /></a></p>
<div class="blog_h2"><span class="graybg">Cilium组件</span></div>
<div class="blog_h3"><span class="graybg">Agent</span></div>
<p>cilium-agent在集群的每个节点上运行，它通过K8S或API接收描述网络、服务负载均衡、网络策略、可观察性的配置信息。</p>
<p>cilium-agent监听来自容器编排系统的事件，从而知晓哪些容器被启动/停止，它管理所有eBPF程序，后者控制所有网络访问。</p>
<p>Cilium利用eBPF实现datapath的过滤、修改、监控、重定向，需要<span style="background-color: #c0c0c0;">Linux 4.8+才能运行，推荐使用4.9.17+内核</span>（因为4.8已经EOL）。Cilium会自动探测内核版本，识别可用特性。</p>
<div class="blog_h3"><span class="graybg">CLI</span></div>
<p>cilium和cilium-agent一起安装，它和cilium-agent的REST API交互，从而探测本地agent的状态。CLI也提供了直接访问eBPF map的工具。</p>
<div class="blog_h3"><span class="graybg">Operator</span></div>
<p>负责集群范围的工作，它不在任何封包转发/网络策略决策的关键路径上，即使operator暂时不可用，集群仍然能正常运作。</p>
<p>根据配置，operator持续不可用一段时间后，可能出现问题：</p>
<ol>
<li>如果需要operator来分配IP地址，则会出现IPAM延迟，因此导致新的工作负载的调度延迟</li>
<li>由于没有operator来更新kvstore的心跳，会导致agent认为kvstore不健康，并重启</li>
</ol>
<div class="blog_h3"><span class="graybg">CNI插件</span></div>
<p>cilium-cni和当前节点上的Cilium API交互，触发必要的datapath配置，以提供容器网络、LB、网络策略。</p>
<div class="blog_h2"><span class="graybg">Hubble组件</span></div>
<div class="blog_h3"><span class="graybg">Server</span></div>
<p>在所有节点上运行，从cilium中抓去eBPF的可观察性数据。它被嵌入在cilium-agent中，以实现高性能和低overhead。它提供了一个gRPC服务，用于抓取flow和Prometheus指标。</p>
<div class="blog_h3"><span class="graybg">Relay</span></div>
<p>hubble-relay是一个独立组件，能够连接到所有Server，通过Server的gRPC API，获取全集群的可观察性数据。这些数据又通过一个API来暴露出去。</p>
<div class="blog_h3"><span class="graybg">CLI</span></div>
<p>hubble是一个命令行工具，能够连接到gRPC API、hubble-relay、本地server，来获取flow events。</p>
<div class="blog_h3"><span class="graybg">GUI</span></div>
<p>hubble-ui能够利用hubble-relay的可观测性数据，提供图形化的服务依赖、连接图。</p>
<div class="blog_h2"><span class="graybg">数据存储</span></div>
<p>Cilium需要一个数据存储，用来在Agent之间传播状态。</p>
<div class="blog_h3"><span class="graybg">K8S CRD</span></div>
<p>默认数据存储。</p>
<div class="blog_h3"><span class="graybg">KV Store</span></div>
<p>外部键值存储，可以提供更好的性能。支持etcd和consul。</p>
<div class="blog_h1"><span class="graybg">概念</span></div>
<div class="blog_h2"><span class="graybg">术语</span></div>
<div class="blog_h3"><span class="graybg">Label</span></div>
<p>标签是定位大的资源集合的一种通用、灵活的方法。每当需要定位、选择、描述某些实体时，Cilium使用标签：</p>
<ol>
<li>Endpoint：从容器运行时、编排系统或者其它资源得到标签</li>
<li>Network Policy：根据标签来选择可以相互通信的一组Endpoint，网络策略自身也基于标签来识别</li>
</ol>
<p>标签就是键值对，值部分可以省略。键具有唯一性，一个实体上不会有两个相同键的标签。键通常仅仅包含字符<code class="docutils literal notranslate"><span class="pre">[a-z0-9-.]</span></code>。</p>
<p>标签从源提取，为了防止潜在的键冲突，Cilium为所有导入的键添加前缀。例如</p>
<ol>
<li><pre class="crayon-plain-tag">k8s:role=frontend</pre>：具有role=frontend的K8S Pod，对应的Cilium端点具有此标签</li>
<li><pre class="crayon-plain-tag">container:user=alex</pre>：通过docker run -l user=alex运行的容器，对应的Cilium端点具有此标签</li>
</ol>
<p>不同前缀含义如下：</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">container</pre>：从本地容器运行时得到的标签<br /><pre class="crayon-plain-tag">k8s</pre>：从Kubernetes得到的标签<br /><pre class="crayon-plain-tag">mesos</pre>：从Mesos得到的标签<br /><pre class="crayon-plain-tag">reserved</pre>：专用于特殊的保留标签<br /><pre class="crayon-plain-tag">unspec</pre>：未指定来源的标签</p>
<p>当通过标签来匹配资源时，使用前缀可以限定资源来源。如果不指定前缀，默认为<pre class="crayon-plain-tag">any:</pre>，标识匹配任何来源的资源。</p>
<div class="blog_h3"><span class="graybg">Endpoint</span></div>
<p>通过为容器分配IP，Cilium让它在网络上可见。多个应容器可能具有相同IP，典型的例子是Pod中的容器。任何享有同一IP的容器，在Cilium的术语里面，叫做端点。</p>
<p>Cilium的默认行为是，同时分配IPv4/IPv6地址给每个端点，你可以使用--enable-ipv4=false这样的选项来禁用某个IP版本。</p>
<p>在内部，Cilium为每个端点，在节点范围内，分配为唯一性的ID。</p>
<p>端点会从关联的容器自动提取端点元数据（Endpoint Metadata）。这些元数据用来安全、策略、负载均衡、路由上识别端点。端点元数据可能来自K8S的Pod标签、Mesos的标签、Docker的容器标签。</p>
<div class="blog_h3"><span class="graybg">Identity</span></div>
<p>任何端点都被分配身份标识（Identity），<span style="background-color: #c0c0c0;">身份标识通过端点的标签确定</span>，并且具有集群范围内的标识符（数字ID）。<span style="background-color: #c0c0c0;">端点被分配的身份标识，和它的安全相关标签匹配</span>，也就是说，具有相同安全相关标签的所有端点，共享同一身份标识。</p>
<p>仅仅安全相关标签用于确定端点的身份标识。<span style="background-color: #c0c0c0;">安全相关标签具有特定前缀</span>，默认情况下安全相关标签以<pre class="crayon-plain-tag">id.</pre>开头。启动cilium-agent时可以<span style="background-color: #c0c0c0;">指定自定义的前缀</span>。</p>
<p>特殊身份标识用于那些不被Cilium管理的端点，这些特殊标识以<pre class="crayon-plain-tag">reserved:</pre>作为前缀：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 60px; text-align: center;">数字ID</td>
<td style="width: 200px; text-align: center;">身份标识</td>
<td style="text-align: center;">描述</td>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>reserved:unknown</td>
<td>无法提取身份标识的任何端点</td>
</tr>
<tr>
<td>1</td>
<td>reserved:host</td>
<td>localhost，任何来自/发往本机IP的流量，都牵涉到该端点</td>
</tr>
<tr>
<td>2</td>
<td>reserved:world</td>
<td>任何集群外的端点</td>
</tr>
<tr>
<td>3</td>
<td>reserved:unmanaged</td>
<td>不被Cilium管理的网络端点，例如在Cilium安装前就存在的Pod</td>
</tr>
<tr>
<td>4</td>
<td>reserved:health</td>
<td>由cilium-agent发起健康检查流量而产生的端点</td>
</tr>
<tr>
<td>5</td>
<td>reserved:init</td>
<td>身份标识尚未提取的端点</td>
</tr>
<tr>
<td>6</td>
<td>reserved:remote-node</td>
<td>集群中所有其它节点的集合</td>
</tr>
</tbody>
</table>
<p>Cilium能够识别一些知名标签，包括k8s-app=kube-dns，并自动分配安全标识。这个特性的目的是，让Cilium顺利的在启用Policy的情况下自举并获得网络连接性。</p>
<p>Cilium利用分布式的KV存储，为身份标识产生数字ID。cilium-agent为会使用身份标识去查询，如果KV存储已经没有对应的数字ID，就会新创建一个。</p>
<div class="blog_h3"><span class="graybg">Node</span></div>
<p>集群的单个成员，每个节点都必须运行cilium-agent。</p>
<div class="blog_h2"><span class="graybg">网络安全</span></div>
<p>Cilium提供多个层次的安全特性，这些特性可以单独或者联合使用。</p>
<div class="blog_h3"><span class="graybg">基于身份标识</span></div>
<p>容器编排系统中倾向于产生大量的Pod，这些Pod具有独立IP。传统的基于IP的网络策略，在容器场景下需要大量的、频繁变动的规则。</p>
<p>Cilium则完全将网络地址和安全策略分开。作为代替，它总是基于Pod的身份标识（通过它的标签提取）应用安全策略。它能够允许任何具有role=frontend标签的Pod访问role=backend的Pod，不管Pod的数量多少。</p>
<div class="blog_h3"><span class="graybg">安全策略</span></div>
<p>如果运行从A到B发起通信，则自动意味着允许B到A的报文传输，但是不意味着B能够发起到A的通信。</p>
<p>安全策略可以在ingress/egress端应用。</p>
<p>如果不提供任何策略，默认行为是允许任何通信。一旦提供一个安全策略规则，则所有不在白名单中的流量都被丢弃。</p>
<div class="blog_h3"><span class="graybg">代理注入</span></div>
<p>Cilium能够透明的为任何网络连接注入L4代理，这是L7网络策略的基础。目前支持的代理实现是<a href="https://docs.cilium.io/en/v1.10/concepts/security/proxy/envoy/">Envoy</a>。</p>
<p>你可以用Go语言编写少量的、用于解析新协议的代码。这种Go代码能够完全利用Cilium提供的高性能的转发到/自Envoy代理的能力、丰富的L7感知策略定义语言、访问日志、基于kTLS的加密流量可观察性。总而言之，作为开发者你只需要使用Go语言编写协议解析代码，其它的事情Cilium+Envoy+eBPF会做好。</p>
<div class="blog_h2"><span class="graybg">网络数据路径概要</span></div>
<div class="blog_h3"><span class="graybg">L1-2</span></div>
<ol>
<li>当网卡接收到封包后，它可以通过PCI桥，将其存放到内存（ring buffer）中</li>
<li>内核中一般化的轮询机制NAPI Poll（epoll、驱动程序都会使用该机制），会拉取到ring buffer中的数据，开始处理</li>
</ol>
<p>&nbsp;</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l12.png"><img class="aligncenter wp-image-37985" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l12.png" alt="datapath-l12" width="788" height="444" /></a></p>
<div class="blog_h3"><span class="graybg">L2</span></div>
<p>几乎所有驱动程序都会实现的<pre class="crayon-plain-tag">drvr_poll</pre>，它会调用第一个BPF程序，即XDP。</p>
<p>如果此程序返回<pre class="crayon-plain-tag">pass</pre>，内核会：</p>
<ol>
<li>调用clean_rx，在此Linux分配skb</li>
<li>如果启用GRO（Generic receive offload），则调用gro_rx，在此封包会被聚合，以一点延迟来换取吞吐量的提升。如果tcpdump时发现不可理解的巨大封包，可能是因为启用了GRO，你看到的是内核给的fake封包</li>
<li>调用receive_skb，开始L2接收处理</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l2.png"><img class="wp-image-37983 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l2.png" alt="datapath-l2" width="952" height="430" /></a></p>
<div class="blog_h3"><span class="graybg">L2-3</span></div>
<p>当调用receive_skb后：</p>
<ol>
<li>如果驱动没有实现XDP支持，则在此调用XDP BPF程序，这里的效率比较低</li>
<li>轮询所有的 socket tap，将包放到正确的（如果存在） tap 设备的缓冲区</li>
<li>调用tc BPF程序。这是Cilium最依赖的挂钩点，实现了修改封包（例如打标记）、重新路由、丢弃封包等操作。这里的BPF程序可能会影响qdisc统计信息，从而影响流量塑形。如果tc BPF程序返回OK，则进入netfilter</li>
<li>netfilter 也会对入向的包进行处理，它是<span style="background-color: #c0c0c0;">网络栈的下半部分</span>，iptables规则越多，对网络栈下半部分造成的瓶颈也就越大</li>
<li>取决于L3协议的类型（几乎都是IP），调用相应L3接收函数并进入网络栈第三层</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l23.png"><img class="wp-image-37981 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l23.png" alt="datapath-l23" width="967" height="435" /></a></p>
<div class="blog_h3"><span class="graybg">L3-4</span></div>
<p>当调用ip_rcv后：</p>
<ol>
<li>首先是netfilter钩子pre_routing，这里会从L4视角处理封包，会执行netfilter中的任何四层规则</li>
<li>netfilter处理完毕后，回调ip_rcv_finish</li>
<li>ip_rcv_finish会立即调用ip_routing对封包进行路由判断：是否位于lookback上，是否能够路由出去。如果Cilium没有使用隧道模式，则会使用到这里的路由功能</li>
<li>如果路由目的地是本机，则会调用ip_local_deliver。进而调用xfrm4_policy</li>
<li>xfrm4_policy负责完成包的封装、解封装、加解密。IPSec就是在此完成</li>
<li>根据L4协议的不同，调用相应的L4接收函数</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l34.png"><img class="wp-image-37979 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l34.png" alt="datapath-l34" width="964" height="353" /></a></p>
<div class="blog_h3"><span class="graybg">L4</span></div>
<p>这里以UDP为例，L4入口函数为udp_rcv：</p>
<ol>
<li>该函数会对封包的合法性进行验证，检查UDP的checksum</li>
<li>封包再次送到xfrm4_policy进行处理。这是因为某些transform policy能够指定L4协议，而此时L4协议才明确</li>
<li>根据端口，查找对应的套接字，然后将skb存放到一个链表s.rcv_q中</li>
<li>最后，调用sk_data_ready，标记套接字有数据待收取</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l4.png"><img class="wp-image-37977 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l4.png" alt="datapath-l4" width="788" height="428" /></a></p>
<div class="blog_h3"><span class="graybg">L4-userspace</span></div>
<ol>
<li>上节提到了，套接字（的等待队列）会被标记为有数据待收取。用户空间程序，通过epoll在等待队列上监听，而因获得通知</li>
<li>用户空间调用udp_recv_msg函数，后者会调用cgroup BPF程序。这种程序用来实现透明的客户端egressing负载均衡</li>
<li>最后是sock_ops BPF程序。用于socket level的细粒度流量塑形。对于某些功能来说这很重要，例如客户端限速</li>
</ol>
<p> <a href="https://blog.gmem.cc/wp-content/uploads/2020/06/l4-us.png"><img class="size-full wp-image-37989 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/l4-us.png" alt="l4-us" width="664" height="654" /></a></p>
<div class="blog_h2"><span class="graybg">BPF挂钩点和对象</span></div>
<p>Linux内核在网络栈中支持一系列的BPF挂钩点，用于挂接BPF程序。Cilium利用这些挂钩点来实现高层次的网络功能。</p>
<div class="blog_h3"><span class="graybg">挂钩点</span></div>
<p>Cilium用到的钩子包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">钩子</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>XDP</td>
<td>
<p>网络路径上最早的、可以软件介入的点，在驱动接收到封包之后，具有最好的封包处理性能</p>
<p>能够快速过滤恶意/非预期的流量，例如DDoS</p>
</td>
</tr>
<tr>
<td>tc ingress/egress</td>
<td>
<p>在封包已经开始最初的处理之后的挂钩点，此时内核L3处理尚未开始，但是已经能够访问大部分的封包元数据</p>
<p>适合进行本节点相关的处理，例如应用L3/L4端点策略，重定向流量到特定端点</p>
</td>
</tr>
<tr>
<td>socket operations</td>
<td>socket operation hook挂钩到特定的cgroup，并且当TCP事件发生时执行。Cilium挂钩到根cgroup，依此实现TCP状态转换的监控，特别是ESTABLISHED状态转换。当一个TCP套接字进入ESTABLISHED状态，并且它具有一个节点本地的对端（可能是一个本地的proxy），则自动执行socket send/recv钩子来进行加速</td>
</tr>
<tr>
<td>socket send/recv</td>
<td>
<p>每当TCP套接字执行send操作时触发，钩子可以探查消息，然后或者丢弃、或者将消息发送到TCP层，或者重定向给另外一个套接字</p>
<p>Cilium使用这种钩子来加速数据路径的重定向</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">网络对象</span></div>
<p>利用上面这些挂钩点，以及虚拟接口（cilium_host, cilium_net）、一个可选的Overlay接口（cilium_vxlan）、内核的crypto支持、以及用户空间代理Envoy，Cilium创建以下类型的网络对象：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">网络对象</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Prefilter</td>
<td>
<p>这类对象运行XDP程序，提供一系列的预过滤规则，获得最大性能的封包过滤</p>
<p>通过Cillium Agent提供的CIDR map，被用<span style="background-color: #c0c0c0;">于快速查找，判定一个封包是否应该被丢弃</span>。例如，假设目的地址不是有效的端点，则应该快速丢弃</p>
</td>
</tr>
<tr>
<td>Endpoint Policy</td>
<td>
<p>这类对象实现Cilium端点策略，它使用一个Map来查询当前封包关联的身份标识，当端点数量很大时，性能不会变差</p>
<p>根据策略，在这一层可能<span style="background-color: #c0c0c0;">丢弃封包、转发给本地端点、转发给Service对象、转发给L7策略对象</span></p>
<p>在Cilium中，这是<span style="background-color: #c0c0c0;">映射封包到身份标识、以及<span style="background-color: #c0c0c0;">应用</span>L3/L4策略</span>的主要对象</p>
</td>
</tr>
<tr>
<td>Service</td>
<td>
<p>这类对象根据每个封包的目的地址来进行Map查找，寻找对应的Service，如果找到了，则封包被转发给Service的某个L3/L4端点</p>
<p>可以和Endpoint Policy对象集成；也可以实现独立的LB</p>
</td>
</tr>
<tr>
<td>L3 Encryption</td>
<td>
<p>在ingress端，L3 Encryption对象标记封包为待解密，随后封包被传递给内核的xfrm（transform）层进行解密，随后解密后的封包传回，并交给网络栈中的其它对象进行后续处理</p>
<p>在egress端，首先根据目的地址进行Map查找，判断是否需要加密，如果是，目标节点上哪些key可用。同时在两端可用的、最近的key被用来加密。封包随后被标记为待解密，传递给内核的xfrm层。加密后的封包，传递给下一层处理，可能是传递给Linux网络栈进行路由，使用overlay的情况下可能直接发起一个尾调用</p>
</td>
</tr>
<tr>
<td>Socket Layer Enforcement</td>
<td>
<p>使用两类钩子：socket operations、socket send/recv，来<span style="background-color: #c0c0c0;">监、控所有Cilium管理的端点（包括L7代理）的TCP连接</span></p>
<p><span style="background-color: #c0c0c0;">socket operations钩子</span>否则<span style="background-color: #c0c0c0;">识别候选的、可加速的套接字</span>。这些<span style="background-color: #c0c0c0;">可加速套接字包括所有本地端点之间的连接、任何发往Cilium代理的连接</span>。可加速套接字的所有封包都会被socket send/recv钩子处理 —— 通过BPF sockmap进行快速重定向</p>
</td>
</tr>
<tr>
<td>L7 Policy</td>
<td>该对象将代理流量重定向给Cilium的用户空间代理，也就是Envoy。Envoy随后要么转发流量，要么根据配置的L7策略生成适当的reject消息</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">BPF Maps</span></div>
<p>Cilium使用了大量的BPF Maps，这些Map创建的时候都进行了容量限制。超过限制后，无法插入数据，因此限制了数据路径的扩容能力。下表显示了默认容量：</p>
<table class="docutils full-width" border="1">
<tbody valign="top">
<tr class="row-even">
<td style="width: 150px; text-align: center;"><strong>Map类别</strong></td>
<td style="text-align: center;"><strong>作用域</strong></td>
<td style="width: 80px; text-align: center;"><strong>默认限制</strong></td>
<td style="text-align: center;"><strong>扩容影响</strong></td>
</tr>
<tr>
<td>Connection Tracking</td>
<td>node<br />endpoint</td>
<td>1M TCP<br />256k UDP</td>
<td>Max 1M concurrent TCP connections, max 256k expected UDP answers</td>
</tr>
<tr class="row-odd">
<td>NAT</td>
<td>node</td>
<td>512k</td>
<td>Max 512k NAT entries</td>
</tr>
<tr class="row-even">
<td>Neighbor Table</td>
<td>node</td>
<td>512k</td>
<td>Max 512k neighbor entries</td>
</tr>
<tr class="row-odd">
<td>Endpoints</td>
<td>node</td>
<td>64k</td>
<td>Max 64k local endpoints + host IPs per node</td>
</tr>
<tr class="row-even">
<td>IP cache</td>
<td>node</td>
<td>512k</td>
<td>Max 256k endpoints (IPv4+IPv6), max 512k endpoints (IPv4 or IPv6) across all clusters</td>
</tr>
<tr class="row-odd">
<td>Load Balancer</td>
<td>node</td>
<td>64k</td>
<td>Max 64k cumulative backends across all services across all clusters</td>
</tr>
<tr class="row-even">
<td>Policy</td>
<td>endpoint</td>
<td>16k</td>
<td>Max 16k allowed identity + port + protocol pairs for specific endpoint</td>
</tr>
<tr class="row-odd">
<td>Proxy Map</td>
<td>node</td>
<td>512k</td>
<td>Max 512k concurrent redirected TCP connections to proxy</td>
</tr>
<tr class="row-even">
<td>Tunnel</td>
<td>node</td>
<td>64k</td>
<td>Max 32k nodes (IPv4+IPv6) or 64k nodes (IPv4 or IPv6) across all clusters</td>
</tr>
<tr class="row-odd">
<td>IPv4 Fragmentation</td>
<td>node</td>
<td>8k</td>
<td>Max 8k fragmented datagrams in flight simultaneously on the node</td>
</tr>
<tr class="row-even">
<td>Session Affinity</td>
<td>node</td>
<td>64k</td>
<td>Max 64k affinities from different clients</td>
</tr>
<tr class="row-odd">
<td>IP Masq</td>
<td>node</td>
<td>16k</td>
<td>Max 16k IPv4 cidrs used by BPF-based ip-masq-agent</td>
</tr>
<tr class="row-even">
<td>Service Source Ranges</td>
<td>node</td>
<td>64k</td>
<td>Max 64k cumulative LB source ranges across all services</td>
</tr>
<tr class="row-odd">
<td>Egress Policy</td>
<td>endpoint</td>
<td>16k</td>
<td>Max 16k endpoints across all destination CIDRs across all clusters</td>
</tr>
</tbody>
</table>
<p>部分BPF Map的容量上限可以通过cilium-agent的命令行选项覆盖：</p>
<p style="padding-left: 30px;">--bpf-ct-global-tcp-max<br />--bpf-ct-global-any-max<br />--bpf-nat-global-max<br />--bpf-neigh-global-max<br />--bpf-policy-map-max<br />--bpf-fragments-map-max<br />--bpf-lb-map-max</p>
<p>如果指定了--bpf-ct-global-tcp-max或/和--bpf-ct-global-any-max，则NAT表（<pre class="crayon-plain-tag">--bpf-nat-global-max</pre>）的大小不能超过前面两个表合计大小的2/3。</p>
<p>使用<pre class="crayon-plain-tag">--bpf-map-dynamic-size-ratio=0.0025</pre>，则cilium-agent在启动时能够动态根据总计内存来调整Map的容量。该选项取值0.0025则0.25%的系统内存用于BPF Map。 该标记会影响消耗大部分内存的Map，包括：</p>
<p style="padding-left: 30px;">cilium_ct_{4,6}_global<br />cilium_ct_{4,6}_any<br />cilium_nodeport_neigh{4,6}<br />cilium_snat_v{4,6}_external<br />cilium_lb{4,6}_reverse_sk</p>
<p>Cilium使用自己的，基于BPF Map实现的连接跟踪表，--bpf-map-dynamic-size-ratio影响容量，但是不会小于131072。</p>
<div class="blog_h2"><span class="graybg">封包生命周期</span></div>
<div class="blog_h3"><span class="graybg">从端点到端点</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium_bpf_endpoint.png"><img class="alignnone wp-image-37657 " src="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium_bpf_endpoint.png" alt="cilium_bpf_endpoint" width="1070" height="1124" /></a></p>
<p>上图包含两个部分：</p>
<ol>
<li>上半部分：本地端点到端点数据流图，显示了Cilium如何配合L7代理进行封包重定向的细节</li>
<li>下半部分：启用了Socket Layer Enforcement后的数据流图。这种情况下，TCP连接的握手阶段，需要遍历Endpoint Policy，直到ESTABLISHED，之后仅仅需要L7 Policy</li>
</ol>
<p>如果启用了L7规则，则流量会被转发给用户空间代理，代理处理完后，转发给目的端点的代理，后者再转发给目的端点的Pod。转发都是由bpf_redir负责，直接修改封包。</p>
<div class="blog_h3"><span class="graybg">端点到Egress</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium_bpf_egres.png"><img class="alignnone wp-image-37661 " src="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium_bpf_egres.png" alt="cilium_bpf_egres" width="1079" height="724" /></a></p>
<p>跨节点的封包流，可能牵涉到overlay，默认情况下overlay接口的名字是cilium_vxlan。</p>
<p>如果需要L3 Encryption，则Endpoint端的tc钩子会将其流量传递给L3 Encryption处理。需要注意tc BPF程序的<span id="crayon-60d3fea497f3d798084325" class="crayon-syntax crayon-syntax-inline  crayon-theme-gmem-github crayon-theme-gmem-github-inline crayon-font-consolas" style="font-size: 15px !important; line-height: 20px !important;"><span class="crayon-pre crayon-code" style="font-size: 15px !important; line-height: 20px !important; -moz-tab-size: 4; -o-tab-size: 4; -webkit-tab-size: 4; tab-size: 4;"><span class="crayon-v">da</span></span></span>模式，能够直接对封包进行修改、转发，而不需要外部的tc action模块。</p>
<p>和端点到端点流量类似，当启用Socket Layer Enforcement时，并且使用L7代理，则对于TCP流量可以避免运行端点和L7代理之间的Endpoint Policy。</p>
<div class="blog_h3"><span class="graybg">Ingress到端点</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2021/06/cilium_bpf_ingress.svg"><img class="alignnone" src="https://blog.gmem.cc/wp-content/uploads/2021/06/cilium_bpf_ingress.svg" alt="" width="1068" height="507" /></a></p>
<p>和端点到端点流量类似，当启用Socket Layer Enforcement时，并且使用L7代理，则对于TCP流量可以避免运行端点和L7代理之间的Endpoint Policy。</p>
<p>这种封包流可以被Prefilter快速处理，决定是否需要丢弃封包、是否需要进行负载均衡处理。</p>
<div class="blog_h2"><span class="graybg">iptables</span></div>
<p>依赖于实际使用的Linux内核版本，Cilium能够利用eBPF datapath全部或部分特性。如果Linux内核版本较低，某些功能可能基于iptables实现。</p>
<p>下图显示了Cilium和kube-proxy安装的iptables规则以及相互关系：<a href="https://blog.gmem.cc/wp-content/uploads/2021/06/kubernetes_iptables.svg"><img class="alignnone" src="https://blog.gmem.cc/wp-content/uploads/2021/06/kubernetes_iptables.svg" alt="" width="1078" height="962" /></a></p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">前提条件</span></div>
<div class="blog_h3"><span class="graybg">内核</span></div>
<p>要求内核版本4.9.17或者更高。</p>
<div class="blog_h3"><span class="graybg">K8S</span></div>
<p>必须启用CNI作为网络插件。</p>
<div class="blog_h2"><span class="graybg">下载命令行工具</span></div>
<pre class="crayon-plain-tag">curl -L --remote-name-all https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz{,.sha256sum}
sha256sum --check cilium-linux-amd64.tar.gz.sha256sum
sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin
rm cilium-linux-amd64.tar.gz{,.sha256sum}</pre>
<div class="blog_h2"><span class="graybg">安装到K8S</span></div>
<div class="blog_h3"><span class="graybg">通过命令行工具</span></div>
<p>参考下面的命令进行安装：</p>
<pre class="crayon-plain-tag"># 安装到当前Kubernetes context
cilium install

cilium install
  --agent-image string              # Cilium Agent镜像
  --operator-image                  # Cilium Operator镜像
  --cluster-id int                  # 多集群模式下唯一ID
  --cluster-name string             # 多集群模式下此集群的名字
  --config strings                  # 添加Cilium配置条目，对应ConfigMap中的一个键值
  --context string                  # 使用的K8S Context
  --datapath-mode                   # 使用的datapath模式
  --disable-check strings           # 禁用指定的校验
  --encryption string               # 所有工作负载流量的加密：disabled（默认） | ipsec | wireguard
  --inherit-ca string               # 从另外一个集群继承/导入CA
  --ipam string                     # IPAM模式
  --kube-proxy-replacement string   # kube-proxy replacement工作模式：disabled（默认） | probe | strict
  -n, --namespace                   # Cilium安装到什么命名空间，默认kube-system
  --native-routing-cidr string      # 直接路由的CIDR，和PodCIDR一致
  --node-encryption                 # 加密所有节点到节点流量
  --restart-unmanaged-pods          # 重启所有没有被Cilium管理的Pod，默认true，保证所有Pod获得Cilium提供的容器网络
  --wait                            # 等待安装完毕，默认true


cilium install  \
  --agent-image=docker.gmem.cc/cilium/cilium:v1.10.1 \
  --operator-image=docker.gmem.cc/cilium/operator-generic:v1.10.1</pre>
<p>如果安装失败，可以通过命令<pre class="crayon-plain-tag">cilium status</pre> 查看整体部署状态，查看日志。</p>
<p>安装完毕后，使用下面的命令来检查状态、进行连通性测试：</p>
<pre class="crayon-plain-tag">cilium status --wait

cilium connectivity test</pre>
<div class="blog_h3"><span class="graybg">通过Helm</span></div>
<pre class="crayon-plain-tag">helm repo add cilium https://helm.cilium.io/



helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system

  # 是否启用调试日志
  --set debug.enabled=true

  # 集群ID，整数，范围1-255，网格中所有集群都必须有唯一ID
  --set cluster.id=1
  # 集群的名字，仅对于集群网格需要
  --set cluster.name=gmem

  # 容器网络路径选项，veth或者ipvlan
  --set datapathMode=veth

  # 禁用隧道，使用直接路由
  --set tunnel=disabled
  # 如果所有节点位于L2网络中，下面的选项用于自动在工作节点之间同步PodCIDR的路由
  # 如果不指定该选项，节点之间的路由不会同步，会出现节点A无法访问B上Pod IP的问题
  --set autoDirectNodeRoutes=true
  # 指定可以直接进行路由（访问时不需要进行IP遮掩）的CIDR，对应K8S配置的cluster-cidr（PodCIDR）
  # 禁用隧道后必须手工设置，否则报错
  --set nativeRoutingCIDR=172.27.0.0/16

  # 当ConfigMap改变时，滚动更新cilium-agent
  --set rollOutCiliumPods=false
  # 为cilium-config这个ConfigMap配置额外的键值
  --set extraConfig={}

  # 传递cilium-agent的额外命令行选项
  --set extraArgs=[]

  # 传递cilium-agent的额外环境变量
  --set extraEnv={}
  
  # 是否在Cilium中启用内置BGP支持
  --set bgp.enabled=false
  # 是否分配、宣告LoadBalancer服务的IP地址
  --set bgp.announce.loadbalancerIP=false

  # 强制cilium-agent在init容器中等待eBPF文件系统已挂载
  --set bpf.waitForMount=false
  # 是否预先分配eBPF Map的键值，会增加内存消耗并降低延迟
  --set bpf.preallocateMaps=false
  # TCP连接跟踪表的最大条目数量
  --set bpf.ctTcpMax=524288
  # 非TCP连接跟踪表的最大条目数量
  --set bpf.ctAnyMax=262144
  # 负载均衡表中最大服务条目
  --set bpf.lbMapMax=65536
  # NAT表最大条目数量
  --set bpf.natMax=524288
  # neighbor表最大条目数量
  --set bpf.neighMax=524288
  # 端点策略映射最大条目数量
  --set bpf.policyMapMax=16384
  # 配置所有BPF Map的自动sizing，根据可用内存
  --set bpf.mapDynamicSizeRatio=0.0025
  # 监控通知（monitor notifications）的聚合级别 none, low, medium, maximum
  --set bpf.monitorAggregation=medium
  # 活动连接的监控通知的间隔
  --set bpf.monitorInterval=5s
  # 哪些TCP flag第一次出现在某个连接中，会触发通知
  --set bpf.monitorFlags=all
  # 允许从外部访问集群的ClusterIP
  --set bpf.lbExternalClusterIP=false
  # 即用基于eBPF的IP遮掩支持
  --set bpf.masquerade=true
  # 直接路由模式，是通过宿主机网络栈进行（true），还是（如果内核支持）使用更直接的、高效的eBPF（false）
  # 后者的副作用是，跳过宿主机的netfilter
  --set bpf.hostRouting=true
  # 是否启用基于eBPF的TPROXY，以便在实现L7策略时减少对iptables的依赖
  --set bpf.tproxy=true
  # NodePort反向NAT处理时，是否跳过FIB查找
  --set bpf.lbBypassFIBLookup=true

  # 每当cilium-agnet重启时，清空BPF状态
  --set cleanBpfState=false
  # 每当cilium-agnet重启时，清空所有状态
  --set cleanState=false

  # 和其它CNI插件组成链，可选值none generic-veth portmap
  --set cni.chainingMode=none
  # 让Cilium管理/etc/cni/net.d目录，将其它CNI插件的配置改为*.cilium_bak
  --set cni.exclusive=true
  # 如果你希望通过外部机制将CNI配置写入，则设置为true
  --set cni.customConf=false
  --set cni.confPath: /etc/cni/net.d
  --set cni.binPath: /opt/cni/bin

  # 配置容器运行时集成  containerd crio docker none auto
  --set containerRuntime.integration=none

  # 支持对自定义BPF程序的尾调用
  --set customCalls.enabled=false

  # IPAM模式
  --set ipam.mode=cluster-pool
  # IPv4 CIDR
  --set ipam.operator.clusterPoolIPv4PodCIDR=0.0.0.0/8
  --set ipam.operator.clusterPoolIPv4MaskSize=24
  # IPv6 CIDR
  --set ipam.operator.clusterPoolIPv6PodCIDR=fd00::/104
  --set ipam.operator.clusterPoolIPv6MaskSize=120

  # 配置基于eBPF的ip-masq-agent
  --set ipMasqAgent.enabled=false

  # IP协议版本支持
  --set ipv4.enabled=true
  --set ipv6.enabled=false

  # 如果启用，这重定向、SNAT离开集群的流量
  --set egressGateway.enabled=false

  # 启用监控sidecar
  --set monitor.enabled=false

  # 配置Service负载均衡
  # 是否启用独立的、不连接到kube-apiserver的L4负载均衡器
  --set loadBalancer.standalone=false
  # 负载均衡算法 random或者maglev
  --set loadBalancer.algorithm=random
  # 对于远程后端，LB操作模式 snat, dsr, hybrid
  --set loadBalancer.mode=snat
  # 是否基于XDP来加速服务处理
  --set loadBalancer.acceleration=disabled
  # 是否利用IP选项/IPIP封装，来将Service的IP/端口信息传递到远程后端
  --set loadBalancer.dsrDispatch=opt

  # 是否对从端点离开节点的流量进行IP遮掩
  --set enableIPv4Masquerade=true
  --set enableIPv6Masquerade=true

  # 支持L7网络策略
  --set l7Proxy=true

  # 镜像
  --set image.repository=docker.gmem.cc/cilium/cilium
  --set image.useDigest=false
  --set operator.image.repository=docker.gmem.cc/cilium/operator
  --set operator.image.useDigest=false


# 重启所有被有被cilium管理的Pod
kubectl get pods --all-namespaces -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,HOSTNETWORK:.spec.hostNetwork --no-headers=true | grep '&lt;none&gt;' | awk '{print "-n "$1" "$2}' | xargs -L 1 -r kubectl delete pod</pre>
<div class="blog_h2"><span class="graybg">高级安装</span></div>
<div class="blog_h3"><span class="graybg">使用外部Etcd</span></div>
<p>使用独立的外部Etcd（而非K8S自带的）可以提供更好的性能适用于更大的部署环境。</p>
<p>选用外部Etcd的时机可能是：</p>
<ol>
<li>超过250节点，5000个Pod。或者，在通过Kubernetes evnets进行状态传播时，出现了很高的overhead</li>
<li>你不希望利用CRD来存储Cilium状态</li>
</ol>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  --set etcd.enabled=true \
  --set "etcd.endpoints[0]=http://etcd-endpoint1:2379" \
  --set "etcd.endpoints[1]=http://etcd-endpoint2:2379" \
  --set "etcd.endpoints[2]=http://etcd-endpoint3:2379" \
  # 不使用CRD来存储状态
  --set identityAllocationMode=kvstore


# 使用SSL
kubectl create secret generic -n kube-system cilium-etcd-secrets \
    --from-file=etcd-client-ca.crt=ca.crt \
    --from-file=etcd-client.key=client.key \
    --from-file=etcd-client.crt=client.crt

helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  --set etcd.enabled=true \
  --set etcd.ssl=true \
  --set "etcd.endpoints[0]=https://etcd-endpoint1:2379" \
  --set "etcd.endpoints[1]=https://etcd-endpoint2:2379" \
  --set "etcd.endpoints[2]=https://etcd-endpoint3:2379"</pre>
<div class="blog_h3"><span class="graybg">CNI Chaining</span></div>
<p>CNI Chaining允许联用Cilium和其它CNI插件。联用时某些Cilium高级特性不可用，包括：</p>
<ol>
<li><a href="https://github.com/cilium/cilium/issues/12454">L7策略</a></li>
<li><a href="https://github.com/cilium/cilium/issues/15596">IPsec透明加密</a></li>
</ol>
<p>你需要创建一个CNI配置，使用plugin list：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ConfigMap
metadata:
  name: cni-configuration
  namespace: kube-system
data:
  cni-config: |-
    {
      "name": "generic-veth",
      "cniVersion": "0.3.1",
      "plugins": [
        {
          "type": "calico",
          "log_level": "info",
          "datastore_type": "kubernetes",
          "mtu": 1440,
          "ipam": {
              "type": "calico-ipam"
          },
          "policy": {
              "type": "k8s"
          },
          "kubernetes": {
              "kubeconfig": "/etc/cni/net.d/calico-kubeconfig"
          }
        },
        {
          "type": "portmap",
          "snat": true,
          "capabilities": {"portMappings": true}
        },
        {
          "type": "cilium-cni"
        }
      ]
    }</pre><br />
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace=kube-system \
  --set cni.chainingMode=generic-veth \
  --set cni.customConf=true \
  --set cni.configMap=cni-configuration \
  --set tunnel=disabled \
  --set enableIPv4Masquerade=false \
  --set enableIdentityMark=false</pre>
<div class="blog_h1"><span class="graybg">K8S集成</span></div>
<p>Cilium能够为K8S带来：</p>
<ol>
<li>基于CNI的容器网络支持</li>
<li>基于身份标识实现的NetworkPolicy，用于隔离L3/L4连接性</li>
<li>CRD形式的NetworkPolicy扩展，支持：
<ol>
<li>L7策略，目前支持HTTP、Kafka等协议</li>
<li>Egress策略支持CIDR</li>
</ol>
</li>
<li>ClusterIP实现，提供分布式的负载均衡。完全兼容kube-proxy模型</li>
</ol>
<p>支持的K8S版本为1.16+，内核版本4.9+。</p>
<p>K8S能够自动分配per-node的CIDR，通过kube-controller-manager的命令行选项<pre class="crayon-plain-tag">--allocate-node-cidrs</pre>启用此特性。Cilium会自动使用分配的CIDR。</p>
<div class="blog_h2"><span class="graybg">ConfigMap</span></div>
<p>Cilium使用名为cilium-config的ConfigMap：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ConfigMap
metadata:
  name: cilium-config
  namespace: kube-system
data:
  # The kvstore configuration is used to enable use of a kvstore for state
  # storage.
  kvstore: etcd
  kvstore-opt: '{"etcd.config": "/var/lib/etcd-config/etcd.config"}'

  # Etcd配置
  etcd-config: |-
    ---
    endpoints:
      - https://node-1:31079
      - https://node-2:31079
    trusted-ca-file: '/var/lib/etcd-secrets/etcd-client-ca.crt'
    key-file: '/var/lib/etcd-secrets/etcd-client.key'
    cert-file: '/var/lib/etcd-secrets/etcd-client.crt'

  # 是否让Cilium运行在调试模式下
  debug: "false"
  # 是否启用IPv4地址支持
  enable-ipv4: "true"
  # 是否启用IPv6地址支持
  enable-ipv6: "true"
  # 在启动cilium-agent时，从文件系统中移除所有eBPF状态。这会导致进行中的连接中断、负载均衡决策丢失
  # 所有的eBPF状态将从源（例如K8S或kvstore）重新构造
  # 该选项用于缓和严重的eBPF maps有关的问题，并且在打开、重启cilium-agent后，立即关闭
  clean-cilium-bpf-state: "false"
  # 清除所有Cilium状态，包括钉在文件系统中的eBPF状态、CNI配置文件、端点状态
  # 当前被Cilium管理的Pod可能继续正常工作，但是可能在没有警告的情况下不再工作
  clean-cilium-state: "false"
  # 该选项启用在cilium monitor中的追踪事件的聚合
  monitor-aggregation: none, low, medium, maximum
  # 启用Map条目的预分配，这样可以降低per-packet的延迟，代价是提前的为Map中条目分配内存
  # 如果此选项改变，则cilium-agnet下次重启会导致具有活动连接的端点临时性中断
  preallocate-bpf-maps: "true"</pre>
<p>修改此ConfigMap后，你需要重新启动cilium-agent才能生效。需要注意，K8S的ConfigMap变更可能需要2分钟才能传播到所有节点。</p>
<div class="blog_h2"><span class="graybg">NetworkPolicy</span></div>
<p>K8S标准的NetworkPolicy，可以用来指定L3/L4 ingress策略，以及受限的egress策略。详细参考<a href="/kubernetes-study-note#network-policy">Kubernetes学习笔记</a>。</p>
<div class="blog_h2"><span class="graybg">CiliumNetworkPolicy</span></div>
<p>功能类似于标准的NetworkPolicy，但是提供丰富的多的特性，能够配置L3/L4/L7策略。</p>
<div class="blog_h2"><span class="graybg">L3策略</span></div>
<p>L3策略用于提供端点之间基本的连接性。支持通过以下方式来指定：</p>
<ol>
<li>基于标签：当通信双方端点都被Cilium管理（因而被提取了标签）时，使用此方式。此方式的优点是IP地址之类的易变信息不会编码在策略中</li>
<li>基于服务：自动提取、维护编排系统的服务的后端IP列表（对于K8S就是Service的Endpoint的IP地址列表）。即使端点不会Cilium管理，这种方式也可以避免硬编码IP到策略中</li>
<li>基于实体：实体用于描述那些被归类的、不需要知道其IP地址的端点。例如具有reserved:身份标识的那些端点</li>
<li>基于IP/CIDR：当外部服务不是一个端点时使用</li>
<li>基于DNS：先进行DNS查找，然后转换为IP</li>
</ol>
<div class="blog_h3"><span class="graybg">基于标签</span></div>
<pre class="crayon-plain-tag">## ingress示例

# 允许frontend访问backend
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l3-rule"
spec:
  endpointSelector:
    matchLabels:
      role: backend
  ingress:
  - fromEndpoints:
    - matchLabels:
        role: frontend

# 允许所有端点访问victim
kind: CiliumNetworkPolicy
metadata:
  name: "allow-all-to-victim"
spec:
  endpointSelector:
    matchLabels:
      role: victim
  ingress:
  - fromEndpoints:
    - {}



## egress示例

# 允许frontend访问backend
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l3-egress-rule"
spec:
  endpointSelector:
    matchLabels:
      role: frontend
  egress:
  - toEndpoints:
    - matchLabels:
        role: backend

# 允许frontend访问所有端点
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "allow-all-from-frontend"
spec:
  endpointSelector:
    matchLabels:
      role: frontend
  egress:
  - toEndpoints:
    - {}

# 禁止restricted访问任何端点
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "deny-all-egress"
spec:
  endpointSelector:
    matchLabels:
      role: restricted
  egress:
  - {}</pre>
<p>在设计策略时，通常遵循关注点分离原则。CiliumNetworkPolicy支持设置任何连接性发生所需要的“前提条件”。字段fromRequires用于为任何fromEndpoints指定前提条件。类似的还有toRequires。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "requires-rule"
specs:
  # 对于生产环境下的Pod，允许访问它的端点，必须也在生产环境中（前提条件）
  - description: "For endpoints with env=prod, only allow if source also has label env=prod"
    endpointSelector:
      matchLabels:
        env: prod
    ingress:
    - fromRequires:
      - matchLabels:
          env: prod</pre>
<p>上面这个 fromRequires规则本身不会允许任何流量，它必须和fromEndpoints规则进行“与”才能允许特定流量：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l3-rule"
specs:
  # 配合上面的规则，效果就是，生产环境下的前端组件，可以访问生产环境下的端点
  - description: "For endpoints with env=prod, allow if source also has label role=frontend"
    endpointSelector:
      matchLabels:
        env: prod
    ingress:
    - fromEndpoints:
      - matchLabels:
          role: frontend</pre>
<div class="blog_h3"><span class="graybg">基于服务</span></div>
<p>运行在集群中的服务，可以在Egress规则的白名单中列出：</p>
<pre class="crayon-plain-tag"># 使用服务名
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "service-rule"
spec:
  # 策略控制的目标端点，总是通过标签选择
  endpointSelector:
    matchLabels:
      id: app2
  egress:
  # 允许访问特定服务
  - toServices:
    - k8sService:
        serviceName: myservice
        namespace: default


# 使用服务选择器
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "service-labels-rule"
spec:
  endpointSelector:
    matchLabels:
      id: app2
  egress:
  - toServices:
    - k8sServiceSelector:
        selector:
          matchLabels:
            head: none</pre>
<p>fromEntities用于描述哪些实体可以访问选择的端点； toEntities则用于描述选择的端点能够访问哪些实体。</p>
<div class="blog_h3"><span class="graybg">基于身份标识</span></div>
<p>支持的实体参考前文描述的具有reserved:前缀的特殊身份标识。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "dev-to-host"
spec:
  endpointSelector:
    matchLabels:
      env: dev
  # 允许开发环境端点访问其本机上的实体
  egress:
    - toEntities:
      - host


apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "to-dev-from-nodes-in-cluster"
spec:
  endpointSelector:
    matchLabels:
      env: dev
  # 允许本机、集群远程机器访问开发环境端点
  # 注意，K8S默认允许从宿主机访问任何本地端点，cilium-agnet选项 --allow-localhost=policy可以禁用这默认行为
  ingress:
    - fromEntities:
      - host
      - remote-node


apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "from-world-to-role-public"
spec:
  endpointSelector:
    matchLabels:
      role: public
  # 允许集群外部访问role:public的端点
  ingress:
    - fromEntities:
      - world</pre>
<div class="blog_h3"><span class="graybg">基于CIDR</span></div>
<p>不被Cilium管理的实体，没有标签，不属于端点。这些实体通常是运行在特定子网中的外部服务、VM、裸金属机器。这类实体在策略中，可以用CIDR规则来描述。</p>
<p>CIDR规则不能用在通信两端都是以下之一的场景：</p>
<ol>
<li>被Cilium管理的端点</li>
<li>使用属于集群节点的IP的实体，包括使用host networking的Pod</li>
</ol>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "cidr-rule"
spec:
  endpointSelector:
    matchLabels:
      app: myService
  # 允许访问外部CIDR
  egress:
  - toCIDR:
    - 20.1.1.1/32
  - toCIDRSet:
    - cidr: 10.0.0.0/8
      except:
      - 10.96.0.0/12</pre>
<div class="blog_h3"><span class="graybg">基于DNS</span></div>
<p>使用DNS名称来指定不被Cilium管理的实体也是支持的，由matchName/matchPattern规则给出的DNS信息，会被cilium-agent收集为IP地址。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "to-fqdn"
spec:
  endpointSelector:
    matchLabels:
      app: test-app
  egress:
    # 通过DNS Proxy拦截DNS请求，这样，当应用程序发起对my-remote-service.com的DNS查询时
    # Cilium能够学习到域名对应的IP地址
    - toEndpoints:
      - matchLabels:
          "k8s:io.kubernetes.pod.namespace": kube-system
          "k8s:k8s-app": kube-dns
      toPorts:
        - ports:
           - port: "53"
             protocol: ANY
          # DNS代理允许查询的域名
          rules:
            dns:
              - matchPattern: "*"
    - toFQDNs:
        # 将精确匹配此名称的IP地址插入到网路策略中
        - matchName: "my-remote-service.com"
        # 将匹配此模式的所有名称对应的IP地址插入到网络策略中
        # * 匹配所有域名，导致所有缓存的DNS IPs插入到规则
        # *.gmem.cc 匹配子域名，不匹配gmem.cc
        - matchPattern: "*"</pre>
<p>很多情况下，应用程序打开的长连接，生存期大于DNS的TTL，如果没有发生后续、针对此长连接域名的查询，则DNS缓存会过期。这种情况下，已经建立的长连接会继续运行。DNS缓存的TTL可以通过<pre class="crayon-plain-tag">--tofqdns-min-ttl</pre>配置。</p>
<p>相反的，对于短连接场景，可能由于反复的DNS查询（服务backed by大量主机）导致FQDN映射的IP地址很快增加，到达默认 <pre class="crayon-plain-tag">--tofqdns-max-ip-per-hostname=50</pre>的限制，并导致最旧的IP被剔除。这种情况下，已经建立的短连接也不会受到影响，直到它断开。</p>
<div class="blog_h3"><span class="graybg">关于DNS Proxy</span></div>
<p>DNS代理能够拦截DNS请求，记录IP和域名的对应关系。为了实现拦截，必须配置一个管理DNS请求的策略规则。</p>
<p><span style="background-color: #c0c0c0;">某些常用的容器镜像（例如alpine/musl）将DNS的Refused应答（当DNS代理拒绝某个查询时）看作更一般性的错误</span>，并且停止遍历/etc/resolv.conf的search list。例如，当Pod访问gmem.cc时，它会首先查询gmem.cc.svc.cluster.local.而得到DNS Proxy的Refused应答，停止遍历，不再查询gmem.cc.并且最终导致Pod认为DNS查询失败。</p>
<p>要解决此问题，可以配置<pre class="crayon-plain-tag">--tofqdns-dns-reject-response-code</pre>，默认值是refused，可以改为nameError，这样DNS代理会返回NXDomain应答。</p>
<div class="blog_h2"><span class="graybg">L4策略</span></div>
<p>主要是在L3的基础上，进行端口限制。</p>
<div class="blog_h3"><span class="graybg">限制端口</span></div>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l4-rule"
spec:
  endpointSelector:
    matchLabels:
      app: myService
  # 允许myService访问80端口
  egress:
    - toPorts:
      - ports:
        - port: "80"
          protocol: TCP</pre>
<div class="blog_h3"><span class="graybg">标签依赖的端口限制</span></div>
<p>下面的例子，允许针对特定标签（所关联的端点）的端口的访问：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l4-rule"
spec:
  endpointSelector:
    matchLabels:
      role: backend
  # 允许frontend服务访问backend服务的80端口
  ingress:
  - fromEndpoints:
    - matchLabels:
        role: frontend
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP</pre>
<div class="blog_h3"><span class="graybg">CIDR依赖的端口限制</span></div>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "cidr-l4-rule"
spec:
  endpointSelector:
    matchLabels:
      role: crawler
  # 允许爬虫访问192.0.2.0/24的80端口
  egress:
  - toCIDR:
    - 192.0.2.0/24
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP</pre>
<div class="blog_h2"><span class="graybg">L7策略</span></div>
<p>目前Cilium支持的L7协议很有限，仅仅HTTP和Kafka（beta）。</p>
<div class="blog_h3"><span class="graybg">HTTP</span></div>
<p>策略可以根据URL路径、HTTP方法、主机名、HTTP头来设置。</p>
<pre class="crayon-plain-tag"># 限定URL路径
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule1"
spec:
  description: "Allow HTTP GET /public from env=prod to app=service"
  endpointSelector:
    matchLabels:
      app: service
  # 允许生产环境访问service的/public
  ingress:
  - fromEndpoints:
    - matchLabels:
        env: prod
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
      rules:
        http:
        - method: "GET"
          path: "/public"


# 限定URL和请求头
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l7-rule"
spec:
  endpointSelector:
    matchLabels:
      app: myService
  ingress:
  - toPorts:
    - ports:
      - port: '80'
        protocol: TCP
      rules:
        http:
        - method: GET
          path: "/path1$"
        - method: PUT
          path: "/path2$"
          headers:
          - 'X-My-Header: true'</pre>
<div class="blog_h3"><span class="graybg">Deny策略</span></div>
<p>用于明确的拒绝特定的流量，<span style="background-color: #c0c0c0;">优先级比Allow策略（CiliumNetworkPolicy/CiliumClusterwideNetworkPolicy/NetworkPolicy）高</span>，上文提及的所有策略都是Allow策略。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: "external-lockdown"
spec:
  endpointSelector: {}
  # 明确禁止外部访问
  ingressDeny:
  - fromEntities:
    - "world"
  ingress:
  - fromEntities:
    - "all"</pre>
<div class="blog_h2"><span class="graybg">CiliumClusterwideNetworkPolicy</span></div>
<p>类似于上面的CiliumNetworkPolicy，区别是：</p>
<ol>
<li>不限定到某个命名空间，集群范围的</li>
<li>支持使用节点选择器</li>
</ol>
<div class="blog_h3"><span class="graybg">主机策略</span></div>
<p>使用节点选择器，可以将策略应用到特定的一个/一组节点。主机策略仅仅应用到宿主机的初始命名空间，包括使用hostnetwork的Pod。</p>
<p>要支持主机策略，需要使用Helm值： <pre class="crayon-plain-tag">--set devices='{interface}'</pre>、<pre class="crayon-plain-tag">--set hostFirewall=true</pre>。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: "lock-down-ingress-worker-node"
spec:
  # 允许据表标签type=ingress-worker的宿主机的所有指定端口的入站流量
  description: "Allow a minimum set of required ports on ingress of worker nodes"
  nodeSelector:
    matchLabels:
      type: ingress-worker
  ingress:
  - fromEntities:
    - remote-node
    - health
  - toPorts:
    - ports:
      - port: "6443"
        protocol: TCP
      - port: "22"
        protocol: TCP
      - port: "2379"
        protocol: TCP
      - port: "4240"
        protocol: TCP
      - port: "8472"
        protocol: UDP
      - port: "REMOVE_ME_AFTER_DOUBLE_CHECKING_PORTS"
        protocol: TCP</pre>
<div class="blog_h2"><span class="graybg">CiliumEndpoint</span></div>
<p>管理K8S中的Pod的过程中，Cilium会自动创建CiliumEndpoint对象，和对应Pod具有相同的namespace+name。</p>
<p>CiliumEndpoint和<pre class="crayon-plain-tag">cilium endpoint get</pre>命令得到的<pre class="crayon-plain-tag">.status</pre>字段有相同的信息：</p>
<pre class="crayon-plain-tag">kubectl get ciliumendpoints.cilium.io  nginx-0 -o jsonpath="{.status}" | jq
{
  // 通信加密设置
  "encryption": {},
  "external-identifiers": {
    "container-id": "eac9972f57187a7afe7bb3edf97c4e70eff8edff26b6923dda8f398d7e622ec9",
    "k8s-namespace": "default",
    "k8s-pod-name": "nginx-0",
    "pod-name": "default/nginx"
  },
  // 端点ID，每个端点都有唯一的ID
  "id": 2318,
  "identity": {
    // 身份标识
    "id": 34796,
    // 具有相同标签的Pod，共享同一身份标识
    "labels": [
      "k8s:app=nginx",
      "k8s:io.cilium.k8s.policy.cluster=default",
      "k8s:io.cilium.k8s.policy.serviceaccount=default",
      "k8s:io.kubernetes.pod.namespace=default"
    ]
  },
  "networking": {
    "addressing": [
      {
        "ipv4": "172.27.2.23"
      }
    ],
    "node": "10.0.3.1"
  },
  "state": "ready"
}

kubectl -n kube-system exec -it cilium-skvr6 -- cilium endpoint get 2318</pre>
<p>每个cilium-agent会创建一个名为<pre class="crayon-plain-tag">cilium-health-&lt;node-name&gt;</pre>的CiliumEndpoint，表示inter-agent健康检查端点。</p>
<div class="blog_h1"><span class="graybg">Istio集成</span></div>
<p>Cilium和Istio都使用Envoy作为七层代理。</p>
<p>集成Cilium和Istio，可以为启用了mTLS的Istio流量提供L7网络策略。如果不进行集成，则可以在Istio Sidecar之外应用应用L7策略，且不能识别mTLS流量。</p>
<div class="blog_h2"><span class="graybg">cilium-istioctl</span></div>
<p>Cilium增强的Istio版本，可以通过cilium-istioctl安装，当前版本1.8.2：</p>
<pre class="crayon-plain-tag">curl -L https://github.com/cilium/istio/releases/download/1.8.2/cilium-istioctl-1.8.2-linux-amd64.tar.gz | tar xz</pre>
<p>运行下面的命令安装Istio：</p>
<pre class="crayon-plain-tag"># 使用默认的Istio配置安装
cilium-istioctl install -y</pre>
<p>启用Istio自动的Envoy Sidecar注入：</p>
<pre class="crayon-plain-tag">kubectl label namespace default istio-injection=enabled</pre>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">网络策略</span></div>
<div class="blog_h2"><span class="graybg">准备</span></div>
<p>我们在default命名空间下，创建以下Pod，用于测试Cilium的功能：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  labels:
    app: nginx
  name: nginx-0
spec:
  containers:
  - args:
    - -g
    - daemon off;
    command:
    - nginx-debug
    image: docker.gmem.cc/library/nginx:1.19.3
    imagePullPolicy: Always
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP

---

apiVersion: v1
kind: Pod
metadata:
  labels:
    app: nginx
  name: nginx-1
spec:
  containers:
  - args:
    - -g
    - daemon off;
    command:
    - nginx-debug
    image: docker.gmem.cc/library/nginx:1.19.3
    imagePullPolicy: Always
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP

---

apiVersion: v1
kind: Pod
metadata:
  labels:
    app: alpine
  name: alpine
spec:
  containers:
  - args:
    - -c
    - sleep 365d
    command:
    - /bin/sh
    image: docker.gmem.cc/alpine:3.11
    imagePullPolicy: Always
    name: apline

---

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu
  labels:
    app: ubuntu
spec:
  containers:
  - args:
    - -c
    - sleep 365d
    command:
    - /bin/sh
    image: docker.gmem.cc/ubuntu:16.04
    imagePullPolicy: Always
    name: ubuntu</pre>
<p>当Pod就绪后，查看端点状态：</p>
<pre class="crayon-plain-tag"># 在端点所在节点的cilium-agent中执行cilium endpoint list
# kubectl -n kube-system exec -it cilium-skvr6 -- cilium endpoint list | grep -E 'ubuntu|alpine|nginx'

ENDPOINT   POLICY (ingress)   POLICY (egress)   IDENTITY   LABELS (source:key[=value]) IPv6   IPv4           STATUS   
           ENFORCEMENT        ENFORCEMENT                                                                                                              
888        Disabled           Disabled          5371       k8s:app=alpine                     172.27.2.118   ready   
931        Disabled           Disabled          34796      k8s:app=nginx                      172.27.2.97    ready   
1781       Disabled           Disabled          34796      k8s:app=nginx                      172.27.2.148   ready   
2363       Disabled           Disabled          42034      k8s:app=ubuntu                     172.27.2.162   ready</pre>
<p>可以看到两个Nginx的Pod具有相同的身份标识，这是因为它们的标签一样。由于没有应用任何策略，因此ingress/egress policy为Disabled。</p>
<p>在为两个Nginx端点创建一个服务：</p>
<pre class="crayon-plain-tag">kubectl create service clusterip nginx --tcp=80:80</pre>
<p>确认客户端可以访问服务：</p>
<pre class="crayon-plain-tag">kubectl exec alpine -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 200
kubectl exec ubuntu -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 200</pre>
<div class="blog_h2"><span class="graybg">身份感知和HTTP感知</span></div>
<div class="blog_h3"><span class="graybg">身份感知（L3/L4策略）</span></div>
<p>下面我们增加一个策略，允许app=alpine访问app=nginx：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "nginx-ingress"
spec:
  endpointSelector:
    matchLabels:
      app: nginx
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: alpine
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP</pre>
<p>应用上述策略后，通过cilium endpoint list可以看到，两个Nginx的ingress policy为Enabled。</p>
<p>现在，在alpine中还能够访问nginx：</p>
<pre class="crayon-plain-tag">kubectl exec alpine -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 200</pre>
<p>在ubuntu中不能访问：</p>
<pre class="crayon-plain-tag">kubectl exec ubuntu -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 000
# 超时
# command terminated with exit code 7</pre>
<p>这说明策略生效，并且是白名单 —— 如果对某个identity应用了（accept）ingress policy，则只有明确声明的fromEndpoints才具有访问权限。</p>
<div class="blog_h3"><span class="graybg">HTTP感知（L7策略）</span></div>
<p>现在alpine能够访问nginx，假设我们向限制它仅仅能访问/welcome这个URL路径，就需要用到Cilium的L7策略：</p>
<pre class="crayon-plain-tag"># kubectl edit cnp nginx-ingress
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "nginx-ingress"
spec:
  endpointSelector:
    matchLabels:
      app: nginx
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: alpine
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
      # 在L4策略的基础上，添加以下内容
      rules:
        http:
        - method: "GET"
          # 支持正则式，例如/welcome/.*
          path: "/welcome"</pre>
<p>现在，alpine访问index.html时会得到403（禁止访问）错误，而GET /welcome则正常访问：</p>
<pre class="crayon-plain-tag">kubectl exec alpine -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 403

kubectl exec alpine -- curl -s -o /dev/null -I -w "%{http_code}\n" -X GET http://nginx/welcome
# 200</pre>
<p>你可以通过下面的命令对流量进行监控：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system exec -it cilium-skvr6 -- cilium monitor -v --type l7

&lt;- Request http from 0 ([k8s:app=alpine k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default]) 
                to 931 ([k8s:io.cilium.k8s.policy.cluster=default k8s:app=nginx k8s:io.kubernetes.pod.namespace=default k8s:io.cilium.k8s.policy.serviceaccount=default]), 
                identity 5371-&gt;34796, verdict Denied HEAD http://nginx/welcome =&gt; 403

&lt;- Request http from 0 ([k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default k8s:app=alpine k8s:io.cilium.k8s.policy.cluster=default]) 
                to 931 ([k8s:io.cilium.k8s.policy.cluster=default k8s:app=nginx k8s:io.kubernetes.pod.namespace=default k8s:io.cilium.k8s.policy.serviceaccount=default]), 
                identity 5371-&gt;34796, verdict Forwarded GET http://nginx/welcome =&gt; 0
&lt;- Response http to 0 ([k8s:io.kubernetes.pod.namespace=default k8s:app=alpine k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default]) 
                from 931 ([k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.cilium.k8s.policy.cluster=default k8s:app=nginx k8s:io.kubernetes.pod.namespace=default]), 
                identity 5371-&gt;34796, verdict Forwarded GET http://nginx/welcome =&gt; 200</pre>
<div class="blog_h2"><span class="graybg">使用DNS规则</span></div>
<div class="blog_h3"><span class="graybg">锁死外部访问</span></div>
<p>假设我们想仅允许nginx访问docker.gmem.cc，可以使用下面的策略：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: nginx-egress
spec:
  endpointSelector:
    matchLabels:
      app: nginx
  egress:
  # 两个规则
  # 第一个规则：允许访问域名docker.gmem.cc
  - toFQDNs:
    # 支持使用通配符，例如 *.gmem.cc
    - matchName: "docker.gmem.cc"  
  # 第二个规则：允许访问kube-dns
  - toEndpoints:
    - matchLabels:
        "k8s:io.kubernetes.pod.namespace": kube-system
        "k8s:k8s-app": kube-dns
    toPorts:
    - ports:
      - port: "53"
        protocol: ANY
      # 这个规则提示Cilium检查匹配pattern的DNS查询，并将结果缓存
      rules:
        dns:
        - matchPattern: "*"</pre>
<p>第二个规则的作用在于，允许nginx访问kube-dns服务，进行域名查询。同时，让Cilium的DNS Proxy能够记录nginx执行的所有DNS查询，并且记录域名和IP地址的对应关系。</p>
<p>Cilium缓存的DNS查询结果中的IP地址，才是真正放到BPF Map中的、允许访问的白名单。</p>
<p>应用上述策略后，nginx将无法访问任何集群内部服务，除了kube-dns，除非你配置额外的策略。测试一下效果：</p>
<pre class="crayon-plain-tag">kubectl exec nginx-0 -- curl -s -o /dev/null -I -w "%{http_code}\n" --insecure https://docker.gmem.cc         
# 200
kubectl exec nginx-0 -- curl -s -o /dev/null -I -w "%{http_code}\n" --insecure https://blog.gmem.cc
# 000
# command terminated with exit code 7
kubectl exec nginx-0 -- curl -s -o /dev/null -I -w "%{http_code}\n" --insecure http://nginx
# 000
# command terminated with exit code 7</pre>
<div class="blog_h3"><span class="graybg">联用toPorts </span></div>
<p>可以联合使用toFQDNs和toPorts，以限制访问外部服务使用的端口、通信协议：</p>
<pre class="crayon-plain-tag"># ...
  egress:
  - toFQDNs:
    - matchPattern: "*.gmem.cc" 
    toPorts:
    - ports:
      - port: "443"
        protocol: TCP</pre>
<div class="blog_h2"><span class="graybg">拦截和探查TLS </span></div>
<p>Cilium支持透明的探查TLS加密连接的内容。 基于这个能力，即使是HTTPS流量，Cilium也能做到API感知并应用L7策略。这种能力完全基于软件实现，并且是策略驱动的，仅仅探测策略选中的网络连接。</p>
<p>我们需要以下步骤，以实现TLS拦截/探查：</p>
<ol>
<li>创建一个内部使用的CA，并基于此CA创建办法证书，以实现TLS拦截。端点访问外部TLS服务时，请求被Cilium拦截，并使用此内部CA颁发的证书为端点提供TLS服务</li>
<li>使用Cilium网络策略的DNS规则，选择需要拦截的流量</li>
<li>进行TLS探查，例如：
<ol>
<li>利用cilium monitor来探查HTTP请求的详细内容</li>
<li>使用L7策略过滤/修改HTTP请求</li>
<li>通过Hubble进行观察</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">创建CA和证书</span></div>
<pre class="crayon-plain-tag"># 自签名CA证书
openssl genrsa -des3 -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 1825 -out ca.crt

# 生成被探查目标服务，这里是docker.gmem.cc的证书，注意填写正确的Common Name
openssl genrsa -out gmem.cc.key 2048
openssl req -new -key gmem.cc.key -out gmem.cc.csr

# 签名证书
openssl x509 -req -days 360 -in gmem.cc.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
    -out gmem.cc.crt -sha256

# 将证书和密钥写入为secret备用
kubectl create secret tls gmem-tls-data -n kube-system --cert=gmem.cc.crt --key=gmem.cc.key</pre>
<div class="blog_h3"><span class="graybg">将CA加入受信根证书列表</span></div>
<p>上面的自签名CA，需要加到源端点（客户端Pod）的受信任根证书列表：</p>
<pre class="crayon-plain-tag">kubectl cp ca.crt default/ubuntu:/usr/local/share/ca-certificates/ca.crt
kubectl exec ubuntu -- update-ca-certificates</pre>
<p>目标服务的CA证书，则需要写入secret备用。最简单办法是，将系统所有受信任证书的列表，一起写入：</p>
<pre class="crayon-plain-tag">kubectl cp default/ubuntu:/etc/ssl/certs/ca-certificates.crt ca-certificates.crt
kubectl -n kube-system create secret generic tls-orig-data --from-file=ca.crt=./ca-certificates.crt</pre>
<div class="blog_h3"><span class="graybg">创建DNS/TLS感知Egress策略 </span></div>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l7-visibility-tls"
spec:
  endpointSelector:
    matchLabels:
      app: ubuntu
  egress:
  - toFQDNs:
    - matchName: "docker.gmem.cc"
    toPorts:
    - ports:
      - port: "443"
        protocol: "TCP"
      # 第一个TLS连接，也就是Cilium扮演服务端的连接，使用的证书（和密钥）
      terminatingTLS:
        secret:
          namespace: "kube-system"
          name: "gmem-tls-data"
      # 第二个TLS连接，也就是Cilium扮演客户端的连接，使用的受信任证书列表
      originatingTLS:
        secret:
          namespace: "kube-system"
          name: "tls-orig-data"
      # 启用L7策略
      rules:
        http:
        # 允许所有HTTP流量
        - {}
  - toPorts:
    - ports:
      - port: "53"
        protocol: ANY
      rules:
        dns:
          - matchPattern: "*"</pre>
<p>应用上述策略后，尝试从ubuntu访问docker.gmem.cc，然后通过cilium monitor -v --type l7探查发生的流量。</p>
<div class="blog_h2"><span class="graybg">gRPC安全策略</span></div>
<p>gRPC是基于HTTP2协议的，Cilium不支持gRPC的原语，但是gRPC服务/方法是映射到特定URL路径的POST方法的：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule1"
spec:
  endpointSelector:
    matchLabels:
      app: nginx
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: alpine
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
      rules:
        http:
        - method: "POST"
          #      gRPC服务          gRPC方法
          path: "/gmem.UserManager/GetName"</pre>
<div class="blog_h2"><span class="graybg">Kafka安全策略 </span></div>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule2"
spec:
  endpointSelector:
    matchLabels:
      app: kafka
  ingress:
  - fromEndpoints:
    - matchLabels:
        # 允许alpine访问kafka
        app: alpine
    toPorts:
    - ports:
      - port: "9092"
        protocol: TCP
      rules:
        kafka:
        # 允许消费msgs主题
        - role: "consume"
          topic: "msgs"</pre>
<div class="blog_h2"><span class="graybg">Cassanadra安全策略</span></div>
<p>目前Cilium提供了对Apache Cassanadra的Beta支持。</p>
<p>Apache Cassanadra是一种NoSQL数据库，专注于提供高性能的（特别是写）事务能力，同时不以牺牲可用性和可扩容性为代价。Cassanadra以集群方式运行，客户端通过Cassanadra协议与集群通信。</p>
<p>Cilium能理解Cassanadra协议，从而控制客户端可以访问哪些表，可以对表进行哪些操作。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "secure-empire-cassandra"
specs:
    endpointSelector:
      matchLabels:
        app: cass-server
    ingress:
    - fromEndpoints:
      - matchLabels:
          app: alpine
      toPorts:
      - ports:
        - port: "9042"
          protocol: TCP
        rules:
          # Cassanadra协议
          l7proto: cassandra
          l7:
            # 允许的操作
          - query_action: "select"
            # 操作针对的表，正则式。指定表需要&lt;keyspace&gt;.&lt;table&gt;形式
            query_table: "system\\..*"
          - query_action: "select"
            query_table: "system_schema\\..*"
          - query_action: "insert"
            query_table: "attendance.daily_records"</pre>
<div class="blog_h2"><span class="graybg">本地重定向策略</span></div>
<p>所谓本地重定向，是指Pod发向IP地址/Service的流量，被重定向到本机Pod的情况。本地重定向策略管理这种流量 —— 它可以将<span style="background-color: #c0c0c0;">匹配策略的流量重定向到本机</span>。</p>
<p>该特性需要4.19+内核。使用选项 <pre class="crayon-plain-tag">--set localRedirectPolicy=true</pre> 开启该特性。</p>
<p>本地重定向策略对应自定义资源<pre class="crayon-plain-tag">CiliumLocalRedirectPolicy</pre>。以下配置字段：</p>
<ol>
<li><pre class="crayon-plain-tag">ServiceMatcher</pre>：用于被重定向的ClusterIP类型的服务</li>
<li><pre class="crayon-plain-tag">AddressMatcher</pre>：用于目的地是IP地址，不属于任何服务的情况</li>
</ol>
<p>当启用本地重定向策略后，非backend Pod访问frontend时，Cilium BPF数据路径会将frontend地址转换为一个本地backend Pod地址。如果流量从backend Pod发往frontend地址，则不会进行进行转换（导致的结果是访问frontend的原始端点），否则就导致循环。Cilium通过调用sk_lookup_助手函数实现这一逻辑。</p>
<div class="blog_h3"><span class="graybg">根据地址匹配</span></div>
<p>下面这个例子，将发往169.254.169.254:8080的TCP流量，重定向到本机的app=proxy端点的80端口：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumLocalRedirectPolicy
metadata:
  name: "lrp-addr"
spec:
  redirectFrontend:
    addressMatcher:
      ip: "169.254.169.254"
      toPorts:
        - port: "8080"
          protocol: TCP
  redirectBackend:
    localEndpointSelector:
      matchLabels:
        app: proxy
    toPorts:
      - port: "80"
        protocol: TCP</pre>
<div class="blog_h3"><span class="graybg">根据服务匹配</span></div>
<p>下面的例子，如果访问default/my-service，则重定向到本机的app=proxy端点的80端口：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumLocalRedirectPolicy
metadata:
  name: "lrp-svc"
spec:
  redirectFrontend:
    serviceMatcher:
      serviceName: my-service
      namespace: default
  redirectBackend:
    localEndpointSelector:
      matchLabels:
        app: proxy
    toPorts:
      - port: "80"
        protocol: TCP</pre>
<div class="blog_h3"><span class="graybg">限制条件</span></div>
<ol>
<li>策略应用之前，匹配策略的已经存在的连接，不受策略影响</li>
<li>此策略不支持更新，只能删除重建</li>
</ol>
<div class="blog_h3"><span class="graybg">应用场景</span></div>
<p>本地重定向策略的一个应用场景是节点本地DNS缓存。 </p>
<p>节点本地DNS缓存在一个静态的IP地址上监听，配合本地重定向策略，可以拦截来自应用程序Pod的、发往kubed-dns ClusterIP的流量。</p>
<p>策略定义示例：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumLocalRedirectPolicy
metadata:
  name: "node-local-dns"
  namespace: kube-system
spec:
  # 如果访问kube-dns
  redirectFrontend:
    serviceMatcher:
      serviceName: kube-dns
      namespace: kube-system
  redirectBackend:
    # 那么重定向到Node local DNS
    localEndpointSelector:
      matchLabels:
        k8s-app: node-local-dns
    # TCP和UDP都支持
    toPorts:
      - port: "53"
        name: dns
        protocol: UDP
      - port: "53"
        name: dns-tcp
        protocol: TCP</pre>
<div class="blog_h1"><span class="graybg">网络连接</span></div>
<div class="blog_h2"><span class="graybg">路由</span></div>
<div class="blog_h3"><span class="graybg">Encapsulation</span></div>
<p>如果不提供任何配置，Cilium自动运行在overlay（encapsulation）模式下，这种模式对底层网络的要求最小。overlay模式下所有节点组成基于UDP封装的网格。支持的封装协议包括：</p>
<ol>
<li>VxLAN：默认封装模式，占用8472/UDP端口</li>
<li>Geneve：占用6081/UDP端口</li>
</ol>
<p>所有Cilium节点之间的流量都被封装。</p>
<p>overlay模式的优点：</p>
<ol>
<li>简单：集群节点所在的网络，不需要对PodCIDR有任何感知。只要底层网络支持IP/UDP，即可构建出overlay网络</li>
<li>地址空间：由于不依赖底层网络，因而可以使用很大的IP地址范围，支持很大规模的Pod数量</li>
<li>自动配置：在编排系统中，每个节点可以被分配一个IP前缀，并独立进行IPAM</li>
<li>身份标识上下文：利用封装协议，可以为网络封包附带元数据。Cilium利用这种能力，来传输源节点的安全标识信息，让目标节点不必查询封包所属的实体</li>
</ol>
<p>overlay模式的缺点：</p>
<ol>
<li>MTU overhead：由于额外的封装头，导致有效MTU比native-routing小。对于VxLAN每个封包的有效MTU减少50字节。这会导致单个特定网络连接的最大吞吐率减小。使用Jumbo frames则实际影响大大减小</li>
</ol>
<div class="blog_h3"><span class="graybg">Native-Routing</span></div>
<p>配置<pre class="crayon-plain-tag">tunnel: disabled</pre>可以启用此datapath，这种模式下，目的地不是本机的封包，被委托给Linux的路由子系统处理。这要求连接节点的网络能够正确处理路由：</p>
<ol>
<li>要么所有节点直接位于L2网络中，可以配置<pre class="crayon-plain-tag">auto-direct-node-routes: true</pre></li>
<li>要么连接它们的路由器能够处理路由：
<ol>
<li>在云环境下，VPC需要和Cilium进行集成，以获得路由信息。目前主流云厂商已经支持</li>
<li>在支持BGP的路由器的配合下，基于BGP协议分发路由。可以通过kube-router来运行BGP守护程序</li>
</ol>
</li>
</ol>
<p>配置<pre class="crayon-plain-tag">native-routing-cidr: x.x.x.x/y</pre>指定可以进行native-routing的CIDR。</p>
<div class="blog_h2"><span class="graybg">IPAM</span></div>
<p>IPAM负责分配和管理网络端点（容器或其它）的IP地址。Cilium支持多种IPAM模式。</p>
<div class="blog_h3"><span class="graybg">kubernetes</span></div>
<p>使用Kubernetes自带的host-scope IPAM。地址分配委托给每个节点进行，per-node的Pod CIDR存放在v1.Node中。</p>
<div class="blog_h3"><span class="graybg">cluster-pool</span></div>
<p>这是默认的IPAM mode，它分配per-node的Pod CIDR，并在每个节点上使用host-scope的分配器来分配IP地址。</p>
<p>此模式和kubernetes类似，区别在于后者在v1.Node资源中存储per-node的Pod CIDR，而Cilium在<pre class="crayon-plain-tag">v2.CiliumNode</pre>中存储此信息。</p>
<p>此模式下，cilium-agent在启动时会等待v2.CiliumNode中的<pre class="crayon-plain-tag">Spec.IPAM.PodCIDRs</pre>字段可用。</p>
<p>通过Helm安装时，使用下面的值来启用此模式：</p>
<pre class="crayon-plain-tag">helm install ...
  --set ipam.mode=cluster-pool

  --set ipam.operator.clusterPoolIPv4PodCIDR=&lt;IPv4CIDR&gt;
  # 调整每个节点的CIDR规模
  --set ipam.operator.clusterPoolIPv4MaskSize=&lt;IPv4MaskSize&gt;

  --set ipam.operator.clusterPoolIPv6PodCIDR=&lt;IPv6CIDR&gt;
  --set ipam.operator.clusterPoolIPv6MaskSize=&lt;IPv6MaskSize&gt;</pre>
<p>在运行时，使用下面的命令查询IP分配错误：</p>
<pre class="crayon-plain-tag">kubectl get ciliumnodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.operator.error}{"\n"}{end}'</pre>
<p>使用下面的命令查看IP分配情况：</p>
<pre class="crayon-plain-tag">cilium status --all-addresses</pre>
<div class="blog_h3"><span class="graybg">crd</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/crd_arch.png"><img class="size-full wp-image-37601 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/crd_arch.png" alt="crd_arch" width="787" height="250" /></a></p>
<p>此模式下，cilium-agent会监听当前节点同名的v2.CiliumNode资源，每当CiliumNode被更新，cilium-agent会利用列在<pre class="crayon-plain-tag">spec.ipam.available</pre>的IP地址来更新本节点的IP池。如果<span style="background-color: #c0c0c0;">已经分配的IP地址从spec.ipam.available中移除</span>，仍然可以正常使用，但是释放后不能重新分配。</p>
<p>当IP被分配出去之后，会记录到<pre class="crayon-plain-tag">status.ipam.inuse</pre>字段。</p>
<p>你需要开发一个Operator，将IP地址分配给特定节点，此模式提供了很大的灵活性。</p>
<div class="blog_h2"><span class="graybg">IP遮掩</span></div>
<p>对于IPv4，容器访问外部流量时Cilium会自动进行SNAT，替换源地址为节点的IP地址。对于IPv6，IP遮掩仅在iptables模式下被支持。</p>
<p>使用选项<pre class="crayon-plain-tag">enable-ipv4-masquerade: false</pre>和<pre class="crayon-plain-tag">enable-ipv6-masquerade: false</pre>可以改变上述默认行为。</p>
<p>如果Pod IP在节点网络中可以路由，可以配置<pre class="crayon-plain-tag">native-routing-cidr</pre>，如果<span style="background-color: #c0c0c0;">目的地址</span>在此CIDR中，则不进行IP遮掩。</p>
<p>Cilium支持多种IP遮掩的实现模式。</p>
<div class="blog_h3"><span class="graybg">ebpf</span></div>
<p>最高效的实现，要求内核版本4.19+，默认启用。对应Helm值<pre class="crayon-plain-tag">bpf.masquerade=true</pre>。当前版本此特性依赖<a href="https://docs.cilium.io/en/v1.10/gettingstarted/kubeproxy-free/#kubeproxy-free">BPF NodePort</a>特性。</p>
<p>基于eBPF的IP遮掩，只能发生在挂钩了eBPF masquerading程序的节点出口设备上。哪些出口设备进行挂钩，可以通过Helm值<pre class="crayon-plain-tag">devices</pre>指定，如果不指定则基于<a href="https://docs.cilium.io/en/v1.10/gettingstarted/kubeproxy-free/#nodeport-devices">BPF NodePort device detection metchanism</a>自动选择。</p>
<p>使用cilium status命令可以检查哪些设备挂钩了：</p>
<pre class="crayon-plain-tag">kubectl exec -it -n kube-system cilium-xxxxx -- cilium status | grep Masquerading
#                                       已挂钩设备     不遮掩的CIDR
# Masquerading:   BPF (ip-masq-agent)   [eth0, eth1]  10.0.0.0/16</pre>
<p>该模式支持TCP/UDP/ICMP这三类IPv4的L4协议，其中ICMP仅仅支持Echo请求/应答。</p>
<p>除了配置native-routing-cidr，你还可以配置Helm值<pre class="crayon-plain-tag">ipMasqAgent.enabled=true</pre>，更细粒度的控制，访问哪些目的IP时不需要进行遮掩。这个能力是依靠Cilium开发的eBPF版本的<a href="https://github.com/kubernetes-sigs/ip-masq-agent">ip-masq-agent</a>来实现的。</p>
<div class="blog_h3"><span class="graybg">iptables</span></div>
<p>遗留模式，支持在所有版本的内核上运行。</p>
<div class="blog_h2"><span class="graybg">IP分片处理</span></div>
<p>默认情况下，Cilium配置eBPF数据路径，进行IP分片跟踪，以允许不支持分段的协议能透明的通过网络传输大报文。</p>
<p>IP分片跟踪在eBPF中通过LRU Map实现，要求4.10+内核。该特性通过以下选项启用：</p>
<ol>
<li><pre class="crayon-plain-tag">enable-ipv4-fragment-tracking</pre>：启用或禁用IPv4分片跟踪，默认启用</li>
<li><pre class="crayon-plain-tag">bpf-fragments-map-max</pre>：控制使用IP分配的活动并发连接的数量</li>
</ol>
<p>UDP这样的协议，它没有TCP那种分段和重组的能力，大报文只能依赖于IP层的分片机制。由于IP分片缺乏重传机制，因此大UDP报文一旦丢失一个片段，就需要整个报文的重传。</p>
<div class="blog_h2"><span class="graybg">BGP</span></div>
<div class="blog_h3"><span class="graybg">集成BIRD</span></div>
<p>BIRD是一个开源软件，支持BGP协议。利用BIRD可以将Cilium管理的端点暴露到集群外部。</p>
<p>通过下面的命令安装bird2：</p>
<pre class="crayon-plain-tag"># Ubuntu
sudo apt install bird2
# CentOS
yum install -y bird2

sudo systemctl enable bird
sudo systemctl restart bird</pre>
<p>节点配置文件示例：</p>
<pre class="crayon-plain-tag"># 其它节点只是ID不同
router id 10.0.3.1;

debug protocols all;

# 如果使用直接路由模式
filter cnionly {
    if net ~ 172.27.0.0/16 &amp;&amp; ifname != "cilium_host" then accept;
    else reject;
}

protocol kernel {
    learn;
    scan time 10;
    ipv4 {
        import none;            # 如果使用隧道模式
        import filter cnionly;  # 如果使用直接路由模式
        export none;
    };
}

protocol device {
    scan time 5;
}

# 直接添加到BIRD的路由表
protocol static {
    ipv4;
    # 宣告Pod CIDR
    route 172.27.0.0/16 via "cilium_host";  # 如果使用隧道模式
    # 宣告ClusterIP CIDR。不能和kube-proxy replacement联用，因为后者不允许集群外访问ClusterIP
    route 10.96.0.0/24  via "eth0";
    # 宣告LoadBalancer CIRD
    route 10.0.10.0/24 via "eth0";
}

# 连接到上游路由器，并宣告上面的静态路由
protocol bgp k8s {
    local as 65000;
    neighbor 10.0.0.1 as 65000;
    direct;
    ipv4 {
        export all;
    };
}
# 查看路由     birdc show route
# 查看BGP状态  birdc show protocols all k8s</pre>
<p>上游路由（反射器）配置示例：</p>
<pre class="crayon-plain-tag">log syslog all;

router id 10.0.0.1;

debug protocols all;

protocol kernel {
    scan time 10;
    ipv4 {
        import none;
        export all;
    };
}

protocol device {
    scan time 5;
}

protocol bgp k8s {
    local as 65000;
    neighbor range 10.0.3.0/24 as 65000;
    direct;
    rr client;
    ipv4 {
        import all;
        export all;
    };
}</pre>
<p>可以启用双向转发检测（Bidirectional Forwarding Detection，BFD），以加入路径故障检测（path failure detection）。BFD由一系列几乎独立的BFD会话组成，每个会话在<span style="background-color: #c0c0c0;">双方都启用了</span>BFD的路由器之间进行双向单播路径的监控。监控方式是周期性的、双向发送控制封包。</p>
<p>BFD不会进行邻居发现，BFD会话是按需（例如被BGP协议请求）创建的。</p>
<pre class="crayon-plain-tag">protocol bfd {
    interface "eth*" {
        min rx interval 100 ms;
        min tx interval 100 ms;
        idle tx interval 300 ms;
        multiplier 10;
    };
    # 不需要按需创建，直接初始化和这些邻居的BFD会话
    neighbor 10.0.3.2;
    neighbor 10.0.3.3;
}

protocol bgp k8s {
    # BGP支持使用BFD来发现邻居是否存活
    bfd on;
}</pre>
<p>下面的命令查看BFD会话状态：</p>
<pre class="crayon-plain-tag">birdc show bfd sessions

bfd1:
IP address                Interface  State      Since         Interval  Timeout
10.0.3.2                  virbr0     Up         11:56:01.055    0.100    1.000
10.0.3.1                  virbr0     Up         11:56:00.094    0.100    1.000
10.0.3.3                  virbr0     Up         11:56:00.389    0.100    1.000</pre>
<p>为了某些特殊目的，例如L4负载均衡，你需要在多个节点上配置Pod CIDR的静态路由，并且在Bird中配置<a href="/network-faq#ecmp">ECMP（Equal-cost multi-path）路由</a>。</p>
<pre class="crayon-plain-tag">protocol kernel {
    merge paths yes limit 3;
}</pre>
<div class="blog_h3"><span class="graybg">宣告LoadBalancerIP</span></div>
<p>Cilium可以原生支持，将LoadBalancer服务分配IP地址、并通过BGP协议将地址宣告出去。是否宣告LoadBalancer服务的IP，取决于服务的externalTrafficPolicy设置。</p>
<p>使用下面的Helm值启用该特性：<pre class="crayon-plain-tag">--set bgp.enabled=true --set bgp.announce.loadbalancerIP=true</pre>。该特性依赖于MetalLB。</p>
<p>添加bgp-config这个ConfigMap，参考：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ConfigMap
metadata:
  name: bgp-config
  namespace: kube-system
data:
  config.yaml: |
    peers:
      - peer-address: 10.0.0.1
        peer-asn: 65000
        my-asn: 65000
    address-pools:
      - name: default
        protocol: bgp
        addresses:
          - 10.0.10.0/24 </pre>
<div class="blog_h2"><span class="graybg">IPVLAN模式</span></div>
<div class="blog_h3"><span class="graybg">VETH vs IPVLAN<br /></span></div>
<p>容器通常都使用虚拟设备，例如veth对，作为连接初始命名空间的桥梁。通过在宿主机端veth挂钩tc ingress钩子，Cilium能够监控容器的任何流量。</p>
<p>veth对处理流量时，需要两次通过网络栈，相比起ipvlan有性能上的劣势。对于两个在同一节点上的容器veth端点，一个封包需要4次通过网络栈。</p>
<p>Cilium CNI也支持L3/L3S的ipvlan，这种模式下，宿主机物理设备作为ipvlan master，而容器端的ipvlan虚拟设备是slave。使用ipvlan时<span style="background-color: #c0c0c0;">将封包从其它网络命名空间推入ipvlan slave设备时消耗更少的资源，因而可能改善网络延迟</span>。使用ipvlan时Cilium在容器命名空间中挂钩BPF程序到ipvlan slave设备的egress钩子，以便应用L3/L4策略（因为初始命名空间下所有容器共享单个设备）。同时挂钩到ipvlan master的tc ingress钩子，可以对节点的所有入站流量应用网络策略。</p>
<p>为了支持老版本的不支持ipvlan hairpin模式的内核，Cilium在ipvlan slave设备（位于容器网络命名空间）的tc gress上挂钩了BPF程序。</p>
<p>当前版本的ipvlan支持有以下限制：</p>
<ol>
<li>NAT64不被支持</li>
<li>基于Envoy的L7 Policy不被支持</li>
<li>容器到host-local的通信不被支持</li>
<li>Service不支持LB到本地端点</li>
</ol>
<div class="blog_h3"><span class="graybg">启用IPVLAN</span></div>
<p>Cilium默认使用veth提供容器网络连接。你可以选用Beta支持的IPVLAN，目前尚未提供的特性包括：</p>
<ol>
<li>IPVLAN L2模式</li>
<li>L7策略支持</li>
<li>FQDN策略支持</li>
<li>NAT64</li>
<li>IPVLAN+隧道</li>
<li>基于eBPF的IP遮掩</li>
</ol>
<p>这些特性将在未来版本提供。</p>
<p>由于使用IPVLAN L3模式，需要4.12+的内核。如果使用L3S模式（流量经过宿主机网络栈因而被netfilter处理），这需要修复<a href="https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git/commit/?id=d5256083f62e2720f75bb3c5a928a0afe47d6bc3">d5256083f62e</a>（4.19.20）的稳定版内核。</p>
<p>参考下面的方式进行安装：</p>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  # 启用IPVLAN
  --set datapathMode=ipvlan \
  # 选择IPVLAN主设备，要求所有节点主设备名字相同
  --set ipvlan.masterDevice=eth0 \
  # IPVLAN数据路径目前仅支持直接路由，因此必须禁用tunnel
  --set tunnel=disabled  \
  # 要让IPVLAN跨节点工作，每个主机都必须安装正确的路由
  # 路由要么手工设置，要么由Cilium自动安装。对于后者，设置：
  --set autoDirectNodeRoutes="true" \
  # 下面的选项，用于控制是否安装iptables规则，这些规则主要用于和kube-proxy交互
  # 如果设置为false，则不安装，并且IPVLAN工作在L3模式
  # 默认值为true，IPVLAN工作在L3S模式，初始命名空间的netfilter会对容器封包进行过滤
  --set installIptablesRules="true"  \
  # 对所有离开IPVLAN master设备的流量进行IP遮掩
  --set masquerade="true"</pre>
<p>IPVLAN L3模式中宿主机的netfilters钩子被绕过，因此无法进行IP遮掩，必须使用L3S模式（会降低性能）。</p>
<div class="blog_h2"><span class="graybg">透明加密（IPsec）</span></div>
<p>Cilium支持使用IPsec/WireGuard透明的加密：</p>
<ol>
<li>Cilium管理的宿主机之间</li>
<li>Cilium管理的端点之间</li>
</ol>
<p>的流量。</p>
<p>为了确定某个连接是否可以被加密，Cilium需要明确封包目的地址是否是受管理的端点。在明确之前，流量可能不被加密。</p>
<p>同一主机内部的流量不会被加密。</p>
<p>如果在其它CNI插件之上链接Cilium，则目前无法支持透明加密特性。</p>
<div class="blog_h3"><span class="graybg">生成和导入PSK</span></div>
<pre class="crayon-plain-tag">kubectl create -n kube-system secret generic cilium-ipsec-keys \
    --from-literal=keys="3 rfc4106(gcm(aes)) $(echo $(dd if=/dev/urandom count=20 bs=1 2&gt; /dev/null | xxd -p -c 64)) 128"</pre>
<div class="blog_h3"><span class="graybg">启用加密</span></div>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  # 启用Pod之间流量的加密
  --set encryption.enabled=true \
  # 启用节点流量加密（Beta）
  --set encryption.nodeEncryption=false \
  # 算法，默认ipsec
  --set encryption.type=ipsec    \
  # 如果启用直接路由（不使用隧道），则不指定下面选项的时候，会查询路由表，选择默认路由对应的网络接口
  --set encryption.ipsec.interface=ethX</pre>
<div class="blog_h2"><span class="graybg">宿主机可达服务</span></div>
<p>通过本节的配置，可以让服务从初始命名空间无需NAT的访问。</p>
<p>此特性要求4.19.57, 5.1.16, 5.2.0等版本以上的内核。如果仅要支持TCP（不支持UDP）则需要4.17.0。</p>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  # 启用此特性
  --set hostServices.enabled=true \
  # 仅仅支持TCP
  --set hostServices.protocols=tcp</pre>
<p>此特性的工作原理：在connect系统调用（TCP, connected UDP），或者 sendmsg/recvmsg系统调用（UDP）时，Cilium会检查目的地址，如果它是一个Service IP，则<span style="background-color: #c0c0c0;">直接将目的地址更换为一个后端的地址</span>。这样，套接字实际上会直接连接真实后端，<span style="background-color: #c0c0c0;">不会在更低层次的数据路径上发生NAT</span>，也就是对数据路径的更低层次透明。</p>
<p>宿主机可达服务，允许从宿主机/Pod中，以多种IP:NODE_PORT访问到NodePort服务。这些IP包括：环回地址、服务ClusterIP、节点本地接口（除了docker*）地址。</p>
<div class="blog_h2"><span class="graybg">kube-proxy replacement<br /></span></div>
<p>Cilium能够完全代替kube-proxy。此特性依赖“宿主机可达服务”，因此对内核有着相同的要求。Cilium还利用5.3/5.8添加的额外特性，进行了更进一步的优化。</p>
<div class="blog_h3"><span class="graybg">移除kube-proxy</span></div>
<p>基于kubeadm安装K8S时，可以用下面的命令跳过kube-proxy：</p>
<pre class="crayon-plain-tag">kubeadm init --skip-phases=addon/kube-proxy</pre>
<p>需要注意：如果节点有多网卡，确保kubelet的<pre class="crayon-plain-tag">--node-ip</pre>设置正确，否则Cilium可能无法正常工作。</p>
<p>如果集群已经安装了kube-proxy，可以使用下面的命令移除：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system delete ds kube-proxy
# 删除cm，可以防止升级K8S（1.19+）时候重新安装kube-proxy
kubectl -n kube-system delete cm kube-proxy</pre>
<div class="blog_h3"><span class="graybg">启用kube-proxy replacement<br /></span></div>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
    --namespace kube-system \
    # 代替kube-proxy，取值：
    #   strict，如果内核不支持，则导致cilium-agent退出
    #   probe，探测内核特性，自动禁用不支持的特性子集。该取值假设kube-proxy不被删除，作为可能的fallback
    --set kubeProxyReplacement=strict \
    # 替换为API Server的地址和端口
    --set k8sServiceHost=10.0.3.1 \
    --set k8sServicePort=6443</pre>
<p>使用如上命令安装的Cilium，可以作为ClisterIP、NodePort、LoadBalancer，以及具有externalIP的服务的控制器。在此之上，eBPF kube-proxy replacement<span style="background-color: #c0c0c0;">还能够支持容器的hostPort，从而不再需要portmap</span>。</p>
<p>kube-proxy replacement同时支持直接路由和隧道模式。</p>
<p>使用下面的命令可以验证kube-proxy replacement已经正常安装：</p>
<pre class="crayon-plain-tag"># kubectl exec -it -n kube-system cilium-ch5qk --  cilium status --verbose 
# ...
KubeProxyReplacement:   Strict   [eth0 10.0.3.2 (Direct Routing)]
# ...
KubeProxyReplacement Details:
  Status:                Strict
  Socket LB Protocols:   TCP, UDP
  Devices:               eth0 10.0.3.2 (Direct Routing)
  Mode:                  SNAT
  Backend Selection:     Random
  Session Affinity:      Enabled
  XDP Acceleration:      Disabled
  Services:
  - ClusterIP:      Enabled
  - NodePort:       Enabled (Range: 30000-32767) 
  - LoadBalancer:   Enabled 
  - externalIPs:    Enabled 
  - HostPort:       Enabled</pre>
<div class="blog_h3"><span class="graybg">磁悬浮一致性哈希</span></div>
<p>kube-proxy replacement支持<a href="https://storage.googleapis.com/pub-tools-public-publication-data/pdf/44824.pdf">磁悬浮（Maglev）一致性哈希算法</a>的变体，作为负载均衡算法。一致性哈希是一类算法，它将后端（RS）计算哈希值后，分布在一个环上。进行负载均衡时，对5元组计算哈希，然后看落在环上哪两个RS之间，取哈希值较小的RS作为LB目标。磁悬浮算法通过将每个RS在环上映射多次，减少当RS数量增加/减少时，必须映射到其它RS的五元组的数量。自然，减少一个RS之后，原先映射到其上的5元组必然需要重新映射，磁悬浮的目标是尽量减少除此之外的重新映射</p>
<p>该算法增强了故障时的弹性。新增节点后，在不需要和其它节点同步的前提下，能够对任意指定5元组能够保持相同的、一致性的后端选择；移除节点后，除了那些后端对应被移除节点的5元组，不超过1% difference in reassignments</p>
<p>通过<pre class="crayon-plain-tag">--set loadBalancer.algorithm=maglev</pre>启用</p>
<p>需要注意，<span style="background-color: #c0c0c0;">该LB算法仅用于外部（南北向）流量</span>。对于集群内部（东西向）流量，套接字直接分配到服务的后端，也就是说在TCP connect的时候，目的地址被修改为后端，不会使用该算法</p>
<p>Cilium XDP加速支持磁悬浮一致性哈希算法。</p>
<p>该算法有两个专用的配置项：</p>
<ol>
<li><pre class="crayon-plain-tag">maglev.tableSize</pre>：每单个服务的Maglev查找表的大小。理想值M，应当大大大于期望后端数N的质数。最好大于100*N，以确保当后端发生变化时最多1% difference in reassignments。支持的取值包括251 509 1021 2039 4093 8191 16381 32749 65521 131071。取值16381用于大概160个后端的服务</li>
<li><pre class="crayon-plain-tag">maglev.hashSeed</pre>：用于避免受限于Cilium内置的固定的seed，seed是base64编码的16byte随机数。所有节点必须具有相同的seed</li>
</ol>
<div class="blog_h3"><span class="graybg">保留客户端源IP</span></div>
<p>Cilium基于eBPF的kube-proxy replacement实现了保留客户端源IP的能力。</p>
<p>Service的<pre class="crayon-plain-tag">externalTrafficPolicy</pre>选项决定Cilium的行为：</p>
<ol>
<li>Local：集群内的服务可以相互访问，也可以从没有该服务后端的节点上访问。集群内的端点，不需要SNAT就能实现访问服务时的负载均衡</li>
<li>Cluster：默认值。有多种途径保留客户端源IP。如果仅TCP服务需要暴露到集群外部，可以让kube-proxy replacement运行在DSR/Hybrid模式</li>
</ol>
<div class="blog_h3"><span class="graybg">直接服务器返回（DSR）</span></div>
<p>默认情况下，Cilium的eBPF NodePort实现，在SNAT模式下运作。也就是说，当来自外部的、访问集群服务的流量到达时，如果<span style="background-color: #c0c0c0;">入群节点判断出服务</span>（LoadBalancer/NodePort/其它具有ExternalIP的服务）<span style="background-color: #c0c0c0;">的后端位于其它节点</span>，<span style="background-color: #c0c0c0;">它就需要将请求重定向到远程节点。这个重定向时需要SNAT，将外部流量的源地址换成入群节点的地址</span></p>
<p>这个<span style="background-color: #c0c0c0;">SNAT的代价是，访问链路多了一跳，同时丢失了源IP信息</span>。为了进行reverse SNAT，返回报文还必须经过入群节点，然后传回给外部客户端</p>
<p>设置<pre class="crayon-plain-tag">loadBalancer.mode=dsr</pre>，可以让Cilium的eBPF NodePort实现切换到DSR模式。这种模式下，后端直接应答外部客户端，不经过入群节点。<span style="background-color: #c0c0c0;">这一特性必须和Direct Routing一起使用</span>，也就是不能使用隧道。</p>
<p>DSR模式的另外一个优势是，源IP地址被保留，因此，运行<span style="background-color: #c0c0c0;">在服务后端节点上的Cilium策略，可以正确的根据依据源IP进行过滤</span>。</p>
<p>由于一个后端可能被多个Service引用，<span style="background-color: #c0c0c0;">后端（所在节点的cilium-agent）需要知道生成（直接回复给原始客户端的）应答报文时，使用什么Service IP/Port（作为源地址）</span>。Cilium的解决办法是，<span style="background-color: #c0c0c0;">使用IPv4选项</span>或IPv6 Destination选项扩展，<span style="background-color: #c0c0c0;">将Service IP/Port信息编码到IP头中</span>，代价是MTU变小。对于TCP服务，<span style="background-color: #c0c0c0;">仅仅SYN封包需要编码Service IP/Port信息，因此MTU变小不会有影响</span>。</p>
<p>需要注意，在某些公有云环境下，DSR模式可能无法工作。原因可能是：</p>
<ol>
<li>底层Fabric<span style="background-color: #c0c0c0;">可能丢弃掉Cilium的IP选项</span></li>
<li>某些云实现了源/目的地址检查，你需要禁用此特性DSR才能正常工作</li>
</ol>
<p>为了避免UDP的MTU变小问题，可以设置<pre class="crayon-plain-tag">loadBalancer.mode=hybrid</pre>，这样对于UDP协议，会工作在SNAT模式，对于TCP则工作在DSR模式</p>
<div class="blog_h3"><span class="graybg">NodePort XDP加速</span></div>
<p>对于LoadBalancer/NodePort/其它具有ExternalIP的服务，如果外部流量入群节点上没有服务后端，则入群节点需要将请求转发给其它节点。Cilium 1.8+支持基于XDP进行加速这一转发行为。XDP工作在驱动层，大部分支持10G+bps的驱动都支持native XDP。云上环境中大多数具有SR-IOV变体的驱动也支持native XDP。在裸金属环境下，XDP加速可以和MetalLB这样的LoadBalancer控制器联用</p>
<p>要启用XDP加速，需要设置<pre class="crayon-plain-tag">loadBalancer.acceleration=native</pre>，默认值<pre class="crayon-plain-tag">disabled</pre>。对于大规模环境，可以考虑调优Map的容量：<pre class="crayon-plain-tag">config.bpfMapDynamicSizeRatio</pre></p>
<p>XDP加速可以和loadBalancer.mode：DSR/SNAT/hybrid一起使用。</p>
<div class="blog_h3"><span class="graybg">NodePort设备/端口/绑定设置</span></div>
<p>启用Cilium的eBPF kube-proxy replacement时，默认情况下，LoadBalancer/NodePort/其它具有ExternalIP的服务，可以通过这样的网络接口访问：</p>
<ol>
<li>具有默认路由的接口</li>
<li>被分配的K8S节点的InternalIP / ExternalIP的接口</li>
</ol>
<p>要改变设备，可以配置devices选项，例如<pre class="crayon-plain-tag">devices='{eth0,eth1,eth2}'</pre>。需要注意每个节点的名字必须一致，如果不一致，可以考虑用通配符<pre class="crayon-plain-tag">devices=eth+</pre></p>
<p>如果使用多个网络接口，仅其中单个可用于Cilium节点之间的直接路由。Cilium会选择具有InternalIP / ExternalIP的接口，InternalIP优先。你也可以手工指定直接路由设备<pre class="crayon-plain-tag">nodePort.directRoutingDevice=eth1</pre>，如果该选项中的设备，不在<pre class="crayon-plain-tag">devices</pre>中，Cilium会自动加入</p>
<p>直接路由设备也用于NodePort XDP加速，也就是说该设备的驱动应该支持native XDP</p>
<p>如果kube-apiserver使用了非默认的NodePort范围，则相同的配置必须传递给Cilium，例如<pre class="crayon-plain-tag">nodePort.range="10000\,32767"</pre></p>
<p>如果NodePort返回和内核临时端口范围（net.ipv4.ip_local_port_range）重叠，则Cilium会将NodePort范围附加到保留端口范围（net.ipv4.ip_local_reserved_ports）。这可以避免NodePort服务劫持宿主机本地应用程序发起的（源端口在和NodePort冲突的）连接。要禁用这种端口范围保护的行为，设置<pre class="crayon-plain-tag">nodePort.autoProtectPortRanges=false</pre></p>
<p>默认情况下，NodePort实现禁止应用程序对NodePort服务端口的bind系统调用，应用程序会接收到bind: Operation not permitted 错误。对于5.7+内核，在Pod内部bind不会报此错误。如果需要完全允许（包括老版本内核、5.7+在初始命名空间）bind，可以设置<pre class="crayon-plain-tag">nodePort.bindProtection=false</pre></p>
<div class="blog_h3"><span class="graybg">容器HostPort支持</span></div>
<p>尽管不是kube-proxy的一部分，Cilium的eBPF kube-proxy replacement也原生实现了hostPort，因此不需要使用CNI chaining：<pre class="crayon-plain-tag">cni.chainingMode=portmap</pre></p>
<p>如果启用了eBPF kube-proxy replacement，hostPort就自动支持，不需要额外配置。其它情况下，可以使用<pre class="crayon-plain-tag">hostPort.enabled=true</pre>启用此特性</p>
<p>如果指定hostPort时没有额外指定hostIP，则Pod的端口将通过宿主机用于暴露NodePort服务的那些IP地址，对外暴露出去。包括K8S的InternalIP/ExternalIP、环回地址。如果指定了hostIP则仅仅从该IP暴露，hostIP指定为0.0.0.0效果等于未指定。</p>
<div class="blog_h3"><span class="graybg">kube-proxy混合模式</span></div>
<p>除了完全代替kube-proxy，Cilium的eBPF kube-proxy replacement还可以与自共存，成为混合模式。混合模式的目的是解决某些内核版本不足以实现完全的kube-proxy replacement的问题。</p>
<p>kubeProxyReplacement取值：</p>
<ol>
<li>strict：严格完全替代或者失败</li>
<li>probe：混合模式。自动探测内核，并尽量替代</li>
<li>partial：混合模式。手工指定需要启用哪些eBPF kube-proxy replacement组件。取该值时必须设置<pre class="crayon-plain-tag">enableHealthCheckNodeport=false</pre>，以确保cilium-agent不会启动NodePort健康检查服务器。可以手工开启的特性如下，默认全部false：
<ol>
<li><pre class="crayon-plain-tag">hostServices.enabled</pre></li>
<li><pre class="crayon-plain-tag">nodePort.enabled</pre></li>
<li><pre class="crayon-plain-tag">externalIPs.enabled</pre></li>
<li><pre class="crayon-plain-tag">hostPort.enabled</pre></li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">会话绑定</span></div>
<p>Cilium的eBPF kube-proxy replacement支持K8S服务的会话绑定设置。对于<pre class="crayon-plain-tag">sessionAffinity: ClientIP</pre>，它会确保同一个Pod/宿主机总是被LB到同一个服务后端。会话绑定的默认超时为3h，可通过K8S的<pre class="crayon-plain-tag">sessionAffinityConfig</pre>改变。</p>
<p>会话绑定的依据，取决于请求的来源：</p>
<ol>
<li>对于集群外部发送给服务的请求，源IP地址用于会话绑定</li>
<li>对于集群内部发起的请求，则客户端网络命名空间的cookie用于会话绑定。这个特性5.7+内核支持，用于在socket layer实现会话绑定（此时源IP尚不可用，封包结构还没被内核创建）</li>
</ol>
<p>如果启用了eBPF kube-proxy replacement，则会话绑定默认启用。要启用，设置<pre class="crayon-plain-tag">config.sessionAffinity=false</pre></p>
<p>如果用户内核版本比较老，不支持网络命名空间cookie。则可以使用fallback的in-cluster模式，该模式使用一个固定的cookie，导致同一主机上，所有端点会绑定到某个服务的同一个后端。</p>
<div class="blog_h3"><span class="graybg">健康检查服务器</span></div>
<p>eBPF kube-proxy replacement包含一个health check server。要启用，需要设置<pre class="crayon-plain-tag">kubeProxyReplacementHealthzBindAddr</pre>。例如<pre class="crayon-plain-tag">kubeProxyReplacementHealthzBindAddr='0.0.0.0:10256'</pre>。/healthz端点用于访问健康状态。</p>
<div class="blog_h3"><span class="graybg">LoadBalancer源地址范围检查</span></div>
<p>如果LoadBalancer服务指定了spec.loadBalancerSourceRanges。则eBPF kube-proxy replacement会限制外部流量对服务的访问。仅仅允许spec.loadBalancerSourceRanges指定的CIDR白名单。从集群内部访问时，忽略此字段。</p>
<p>此特性默认启用，要禁用，设置<pre class="crayon-plain-tag">config.svcSourceRangeCheck=false</pre>。</p>
<div class="blog_h3"><span class="graybg">service-proxy-name</span></div>
<p>和kube-proxy类似，eBPF kube-proxy replacement遵从服务的<pre class="crayon-plain-tag">service.kubernetes.io/service-proxy-name</pre>注解。此注解声明什么服务代理（kube-proxy / replacement...）应该管理此服务。</p>
<p>eBPF kube-proxy replacement的服务代理名通过<pre class="crayon-plain-tag">k8s.serviceProxyName</pre>设置。默认值为空，意味着仅仅没有设置service.kubernetes.io/service-proxy-name的服务可以被replacement管理。</p>
<div class="blog_h3"><span class="graybg">限制条件</span></div>
<p>使用Cilium的eBPF kube-proxy replacement时，有很多限制条件需要注意：</p>
<ol>
<li>不能和透明加密一起使用</li>
<li>依赖宿主机可达服务这一特性。该特性需要依赖于eBPF cgroup hooks来实现服务转换。而eBPF中的getpeername需要5.8+内核才能支持。这意味着replacement无法和libceph一起工作</li>
<li>XDP加速仅支持单个设备的hairpin LB场景。如果具有多个网卡，并且cilium自动检测并选择多个网卡，则必须通过devices选项指定一个</li>
<li>DSR NodePort模式目前不能很好的在启用了TCP Fast Open（TFO）的环境下使用，建议切换到SNAT模式</li>
<li>不支持SCTP协议</li>
<li>不支持Pod配置的hostPort和NodePort范围冲突。这种情况下hostPort被忽略，并且cilium-agent会打印警告日志</li>
<li><span style="background-color: #c0c0c0;">不允许从集群外部访问ClusterIP</span></li>
<li>不支持ping ClusterIP，不像IPVS</li>
</ol>
<div class="blog_h2"><span class="graybg">带宽管理器</span></div>
<p>利用Cilium的带宽管理器，可以有效的在EDT（Earliest Departure Time）、eBPF的帮助下，管理每个Pod的带宽占用。</p>
<p>Cilium的带宽管理器，不依赖于CNI chaining，而是在Cilium内部实现的，它不使用<a href="https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#support-traffic-shaping">bandwidth CNI</a>这个插件。出于可扩容性考虑（特别是对于多队列的网卡），不建议使用bandwith CNI插件，因为它基于qdisc TBF而非EDT。</p>
<p>Cilium带宽管理器支持Pod注解<pre class="crayon-plain-tag">kubernetes.io/egress-bandwidth</pre>，它在“原生宿主网络设备”上控制egress流量带宽。不管是直接路由还是隧道，都可以进行流量限制。</p>
<p>Pod注解<pre class="crayon-plain-tag">kubernetes.io/ingress-bandwidth</pre>不被支持，也不推荐使用。</p>
<p>带宽限制天然应该发生在egress以降低/整平在网线上的带宽使用。如果在ingress段进行带宽限制，会额外的、通过ifb设备，在节点的关键fast-path增加一层缓冲队列，这种情况下，流量需要被重定向到ifb设备的egress端以实现塑形。这本质上没有意义，因为流量已经占用了网线山的带宽，节点也已经消耗了资源处理它，唯一的作用就是引入ifb让上层应用遭受带宽限制的痛苦。</p>
<p>带宽管理器需要Linux 5.1+内核。</p>
<p>带宽管理器默认启用，不需要在安装时指定特殊的选项。如果想禁用，设置<pre class="crayon-plain-tag">bandwidthManager=false</pre>。</p>
<p>所谓“原生宿主网络设备”是指具有默认路由的网络接口，或者分配了InternalIP/ExternalIP的接口，分配InternalIP的接口优先。如果要手工指定设置，设置<pre class="crayon-plain-tag">devices</pre>选项。</p>
<p>对Pod进行带宽限制的例子：</p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: Deployment
metadata:
  name: netperf
spec:
  selector:
    matchLabels:
      run: netperf
  replicas: 1
  template:
    metadata:
      labels:
        run: netperf
      annotations:
        kubernetes.io/egress-bandwidth: "10M"
    spec:
      nodeName: foobar
      containers:
      - name: netperf
        image: cilium/netperf
        ports:
        - containerPort: 12865</pre>
<div class="blog_h3"><span class="graybg">限制条件 </span></div>
<p>目前带宽管理器不能和L7策略联用。如果L7策略选择了Pod，则Pod上设置的注解被忽略，不进行带宽限制。</p>
<div class="blog_h2"><span class="graybg">联用Kata Containers</span></div>
<p>Cilium可以和Kata联用，后者提供计算层安全性。根据你使用的容器运行时，配置Cilium：</p>
<ol>
<li>如果使用CRI-O：<pre class="crayon-plain-tag">--set containerRuntime.integration=crio</pre></li>
<li>如果使用CRI-containerd：<pre class="crayon-plain-tag">--set containerRuntime.integration=containerd</pre></li>
</ol>
<p>Kata containers不支持宿主机可达服务特性，因而也不支持kube-proxy replacement的strict模式。</p>
<div class="blog_h2"><span class="graybg">Egress网关</span></div>
<p>出口网关允许将Pod的出口流量重定向到特定的网关节点，功能类似于Istio的出口网关。参考下面的选项启用该特性：</p>
<pre class="crayon-plain-tag">helm upgrade cilium cilium/cilium
   --namespace kube-system \
   --reuse-values \
   --set egressGateway.enabled=true \
   --set bpf.masquerade=true \
   --set kubeProxyReplacement=strict</pre>
<p>你需要配置<pre class="crayon-plain-tag">CiliumEgressNATPolicy</pre>才能让Egress网关对特定端点生效：</p>
<pre class="crayon-plain-tag">apiVersion: cilium.io/v2alpha1
kind: CiliumEgressNATPolicy
metadata:
  name: egress-sample
spec:
  egress:
  - podSelector:
      matchLabels:
        # 如果端点是运行在default命名空间的app=alpine
        app: alpine
        io.kubernetes.pod.namespace: default
    # 也可以用命名空间选择器，匹配多个命名空间（中的所有Pod）
    # namespaceSelector:
    #  matchLabels:
    #    ns: default
  # 并且尝试访问下面的CIDR（集群外部服务）
  destinationCIDRs:
  - 192.168.33.13/32
  # 那么将流量转发给Egress网关，该网关（节点）配置了IP地址192.168.33.100
  # 出集群封包将SNAT为192.168.33.100
  egressSourceIP: "192.168.33.100"</pre>
<p>作为Egress网关的节点，需要在网络接口上配置额外的IP（对应上面的 egressSourceIP）。</p>
<div class="blog_h1"><span class="graybg">集群网格</span></div>
<p>Cluster Mesh将网络数据路径延伸到多个集群，支持以下特性：</p>
<ol>
<li>实现<span style="background-color: #c0c0c0;">所有集群的Pod之间相互连通</span>，不管使用直接路由还是隧道模式。不需要额外的网关节点或代理</li>
<li>支持全局服务，可以在所有集群访问</li>
<li>支持全局性的安全策略</li>
<li>支持跨集群边界通信的透明加密</li>
</ol>
<div class="blog_h2"><span class="graybg">应用场景</span></div>
<div class="blog_h3"><span class="graybg">高可用</span></div>
<p>两个（位于不同Region或AZ的）集群组成高可用，当一个集群的后端服务（不是整个AZ不可用）出现故障时，可以failover到另外一个集群的对等物。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/usecase_ha.png"><img class="size-large wp-image-37965 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/usecase_ha-1024x342.png" alt="usecase_ha" width="710" height="237" /></a></p>
<div class="blog_h3"><span class="graybg">共享服务</span></div>
<p>最初的K8S用法是，倾向于创建巨大的、多租户的集群。而现在，更场景的用法是为每个租户创建独立的集群，甚至为不同类型的服务（例如安全级别不同）创建独立的集群。尽管如此，仍然有一些服务具有共享特征，不适合在每个集群中都部署一份。这类服务包括：日志、监控、DNS、密钥管理，等等。</p>
<p>使用集群网格，可以将共享服务独立部署在一个集群中，租户集群可以访问其中的全局服务。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/usecase_shared_services.png"><img class="size-large wp-image-37969 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/usecase_shared_services-1024x444.png" alt="usecase_shared_services" width="710" height="307" /></a></p>
<div class="blog_h3"><span class="graybg">联合Istio Multicluster<br /></span></div>
<p>Cilium Clustermesh和Istio Multicluster可以相互补充。典型的用法是，<span style="background-color: #c0c0c0;">Cilium提供跨集群的Pod IP可路由性，而这是Istio Multiplecluster所需要的前置条件</span>。</p>
<div class="blog_h2"><span class="graybg">前提条件</span></div>
<ol>
<li>所有集群的Pod CIDR不冲突</li>
<li>所有节点的IP地址不冲突</li>
<li>所有集群的节点，都具有IP层的相互连接性。可能需要创建对等/VPN隧道</li>
<li>集群之间的网络必须允许跨集群通信，到底需要哪些端口本章后续会详述</li>
</ol>
<div class="blog_h2"><span class="graybg">启用网格</span></div>
<div class="blog_h3"><span class="graybg">指定集群标识</span></div>
<p>每个集群都需要唯一的名字和ID：</p>
<pre class="crayon-plain-tag">helm upgrade cilium cilium/cilium \
   --namespace kube-system \
   --reuse-values \
   --set cluster.name=k8s --set cluster.id=27</pre>
<p>注意，如果改变正在运行的集群的ID/名字，其中所有工作负载都需要重新启动。因为ID用于生成安全标识（security identity），安全标识需要重新创建才能创建跨集群的通信。</p>
<pre class="crayon-plain-tag">helm --kube-context tke install cilium cilium/cilium --version 1.10.1                        \
  --namespace kube-system                                                                    \
  --set debug.enabled=true                                                                   \
  --set cluster.id=28                                                                        \
  --set rollOutCiliumPods=true                                                               \
  --set cluster.name=tke                                                                     \
  --set image.repository=docker.gmem.cc/cilium/cilium                                        \
  --set preflight.image.repository=docker.gmem.cc/cilium/cilium                              \
  --set image.useDigest=false                                                                \
  --set operator.image.repository=docker.gmem.cc/cilium/operator                             \
  --set operator.image.useDigest=false                                                       \
  --set certgen.image.repository=docker.gmem.cc/cilium/certgen                               \
  --set hubble.relay.image.repository=docker.gmem.cc/cilium/hubble-relay                     \
  --set hubble.relay.image.useDigest=false                                                   \
  --set hubble.ui.backend.image.repository=docker.gmem.cc/cilium/hubble-ui-backend           \
  --set hubble.ui.backend.image.tag=v0.7.9                                                   \
  --set hubble.ui.frontend.image.repository=docker.gmem.cc/cilium/hubble-ui                  \
  --set hubble.ui.frontend.image.tag=v0.7.9                                                  \
  --set hubble.ui.proxy.image.repository=docker.gmem.cc/envoyproxy/envoy                     \
  --set hubble.ui.proxy.image.tag=v1.18.2                                                    \
  --set etcd.image.repository=docker.gmem.cc/cilium/cilium-etcd-operator                     \
  --set etcd.image.tag=v2.0.7                                                                \
  --set nodeinit.image.repository=docker.gmem.cc/cilium/startup-script                       \
  --set nodeinit.image.tag=62bfbe88c17778aad7bef9fa57ff9e2d4a9ba0d8                          \
  --set clustermesh.apiserver.image.repository=docker.gmem.cc/cilium/clustermesh-apiserver   \
  --set clustermesh.apiserver.image.useDigest=false                                          \
  --set clustermesh.apiserver.etcd.image.repository=docker.gmem.cc/coreos/etcd               \
  --set tunnel=disabled                                                                      \
  --set autoDirectNodeRoutes=true                                                            \
  --set nativeRoutingCIDR=172.28.0.0/16                                                      \
  --set bpf.hostRouting=true                                                                 \
  --set ipam.mode=cluster-pool                                                               \
  --set ipam.operator.clusterPoolIPv4PodCIDR=172.28.0.0/16                                   \
  --set ipam.operator.clusterPoolIPv4MaskSize=24                                             \
  --set fragmentTracking=true                                                                \
  --set bpf.masquerade=true                                                                  \
  --set hostServices.enabled=true                                                            \
  --set kubeProxyReplacement=strict                                                          \
  --set k8sServiceHost=10.2.0.61                                                             \
  --set k8sServicePort=6443                                                                  \
  --set loadBalancer.algorithm=maglev                                                        \
  --set loadBalancer.mode=hybrid                                                             \
  --set bandwidthManager=true</pre>
<div class="blog_h3"><span class="graybg">创建cilium-ca</span></div>
<p>集群网格会基于此CA创建其API Server的数字证书：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system create secret generic --from-file=ca.key=ca.key --from-file=ca.crt=ca.crt cilium-ca </pre>
<div class="blog_h3"><span class="graybg">启用网格</span></div>
<p>需要在组成网格的两个集群中都执行cilium clustermesh enable命令：</p>
<pre class="crayon-plain-tag">cilium clustermesh enable --context k8s --service-type LoadBalancer \
    --apiserver-image docker.gmem.cc/cilium/clustermesh-apiserver:v1.10.1
cilium clustermesh enable --context tke --service-type LoadBalancer \
    --apiserver-image docker.gmem.cc/cilium/clustermesh-apiserver:v1.10.1</pre>
<p>上述命令会：</p>
<ol>
<li>部署clustermesh-apiserver到集群</li>
<li>生成所有必须的数字证书、保存为Secret</li>
<li>自动检测最佳的service类型，以暴露集群网格的控制平面给其它集群。某些时候，service类型不能自动检测，你可手工通过<pre class="crayon-plain-tag">--service-type</pre>指定</li>
</ol>
<p>通过下面的命令等待集群网格组件就绪：<pre class="crayon-plain-tag">cilium clustermesh status --wait</pre>，如果服务类型选择LoadBalancer，该命令也会等待LoadBalancer IP就绪。</p>
<div class="blog_h3"><span class="graybg">连接集群</span></div>
<p>最后一步是连接集群，只需要在网格的一端进行连接即可。对向连接会自动创建：</p>
<pre class="crayon-plain-tag">cilium clustermesh connect --context k8s --destination-context tke</pre>
<p>通过下面的命令等待连接成功：<pre class="crayon-plain-tag">cilium clustermesh status --wait</pre></p>
<div class="blog_h3"><span class="graybg">测试跨集群连接性</span></div>
<pre class="crayon-plain-tag">cilium connectivity test --context k8s --multi-cluster tke</pre>
<p>注意：<span style="background-color: #c0c0c0;">两个集群的Pod网络会被打通，你可以从一个集群直接访问另外一个集群的Pod</span>。<span style="background-color: #c0c0c0;">默认情况下，Cilium不允许从集群外部访问PodCIDR，可以ping但是访问端口会RST</span>。</p>
<div class="blog_h3"><span class="graybg">查看网格状态</span></div>
<pre class="crayon-plain-tag">cilium clustermesh status --context k8s
cilium clustermesh status --context tke</pre>
<div class="blog_h3"><span class="graybg">限制条件</span></div>
<p>目前最多支持相互连接在一起的集群数量为255，未来此限制会放开，当：</p>
<ol>
<li>运行在直接路由模式时</li>
<li>运行在隧道模式，且启用加密时</li>
</ol>
<div class="blog_h2"><span class="graybg">服务发现</span></div>
<p>Cilium的集群网格，支持跨集群的服务发现和负载均衡。</p>
<div class="blog_h3"><span class="graybg">全局服务</span></div>
<p>跨集群的负载均衡，依赖于全局服务。所谓全局服务：</p>
<ol>
<li>在所有集群中具有相同的namespace和name</li>
<li>设置了注解<pre class="crayon-plain-tag">io.cilium/global-service: "true"</pre>，注意，<span style="background-color: #c0c0c0;">所有集群的服务都要添加此注解</span></li>
</ol>
<p>Cilium会自动跨越多个集群进行负载均衡。A集群中的Pod可能访问到B集群的后端。</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Service
metadata:
  name: nginx
  annotations:
    io.cilium/global-service: "true"
    # 下面这个注解是隐含的
    io.cilium/shared-service: "true"
spec:
  type: ClusterIP
  ports:
  - port: 80
  selector:
    app: nginx</pre>
<div class="blog_h3"><span class="graybg">远程服务</span></div>
<p>如果设置<pre class="crayon-plain-tag">io.cilium/shared-service: "false"</pre>，则该服务的端点，仅由远程集群提供。</p>
<div class="blog_h2"><span class="graybg">网络策略</span></div>
<p>CiliumNetworkPolicy、NetworkPolicy自然就能跨集群生效，这是因为Cilium解耦了网络安全和网络连接性。但是这些对象不会自动复制到各集群，你需要手工处理。</p>
<p>下面的网络策略，允许特定端点跨集群的访问服务：</p>
<pre class="crayon-plain-tag"># 允许k8s中的alpine访问tke中的nginx
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "allow-cross-cluster"
spec:
  endpointSelector:
    matchLabels:
      app: alpine
      io.cilium.k8s.policy.cluster: k8s
  egress:
  - toEndpoints:
    - matchLabels:
        app: nginx
        io.cilium.k8s.policy.cluster: tke</pre>
<div class="blog_h3"><span class="graybg">限制条件</span></div>
<p>L7策略仅仅在以下条件下可以跨集群生效：</p>
<ol>
<li>启用直接路由模式，也就是禁用隧道</li>
<li>节点安装了路由，允许路由所有集群的Pod IP</li>
</ol>
<p>第2点，如果节点L2互联，可以通过设置--auto-direct-node-routes=true满足。</p>
<div class="blog_h2"><span class="graybg">集群外工作负载</span></div>
<p>你可以将外部工作负载（例如VM）加入到K8S集群，并且应用安全策略。</p>
<div class="blog_h3"><span class="graybg"> 前提条件</span></div>
<ol>
<li>必须配置基于K8S进行身份标识（identity）分配，即<pre class="crayon-plain-tag">identityAllocationMode=crd</pre>（默认值）</li>
<li>外部工作负载必须基于4.17+的内核，这样它才能访问K8S服务</li>
<li>外部工作负载必须和集群节点IP层互联。如果在同一VPC中运行虚拟机和K8S，通常可以满足。否则，可能需要在K8S集群网络和外部工作负载网络之间进行对等/VPN连接</li>
<li>外部工作负载必须具有唯一的IP地址，和集群内节点不冲突</li>
<li>目前此特性仅在VXLAN隧道模式下测试过</li>
</ol>
<div class="blog_h3"><span class="graybg">启用集群网格</span></div>
<pre class="crayon-plain-tag">cilium install --config tunnel=vxlan ...
cilium clustermesh enable</pre>
<div class="blog_h3"><span class="graybg">测试工作负载</span></div>
<p>必须创建<pre class="crayon-plain-tag">CiliumExternalWorkload</pre>来通知集群，外部工作负载的存在。该自定义资源：</p>
<ol>
<li>为外部工作负载指定命名空间和身份标识标签</li>
<li>名字必须和外部工作负载的主机名（hostname命令输出）一致</li>
<li>为外部工作负载分配一个很小的CIDR</li>
</ol>
<p>可以通过命令创建CiliumExternalWorkload：</p>
<pre class="crayon-plain-tag">#                  vm是子命令external-workload的别名
#                            工作负载名字
#                                      加入的命名空间
#                                              分配的CIDR
cilium clustermesh vm create zircon -n default --ipv4-alloc-cidr 10.0.0.1/32</pre>
<p>下面的命令可以查看现有外部工作负载的状态：</p>
<pre class="crayon-plain-tag">cilium clustermesh vm status</pre>
<p>此时，可以看到 zircon 的<pre class="crayon-plain-tag">IP</pre>状态为<pre class="crayon-plain-tag">N/A</pre>，这提示工作负责尚未加入集群。</p>
<p>下面的命令会生成一个安装脚本：</p>
<pre class="crayon-plain-tag">cilium clustermesh vm install install-external-workload.sh</pre>
<p>该脚本从集群中抽取了TLS证书、其它访问信息 ，可用于在外部工作负责中安装Cilium并连接到你的K8S集群。脚本中嵌入了clustermesh-apiserver服务的IP地址，如果你没有使用LoadBalancer类型而是使用NodePort，则IP是第一个K8S节点的地址。</p>
<p>拷贝install-external-workload.sh到外部工作负载节点，然后执行，该脚本会：</p>
<ol>
<li>创建并运行一个名为cilium的容器</li>
<li>拷贝cilium CLI到文件系统</li>
<li>等待节点连接到集群，集群服务可用。然后修改/etc/resolv.conf，将kube-dns地址设置到其中</li>
</ol>
<p>注意，如果外部工作负载有多个网络接口，在运行脚本之前你需要设置环境变量<pre class="crayon-plain-tag">HOST_IP</pre>。</p>
<p>在外部工作负载执行命令<pre class="crayon-plain-tag">cilium status</pre>检查连接性。</p>
<div class="blog_h1"><span class="graybg">运维</span></div>
<div class="blog_h2"><span class="graybg">状态解读</span></div>
<p>每个cilium-agnet节点的各种状态信息，可以通过cilium status命令获得：</p>
<pre class="crayon-plain-tag"># kubectl -n kube-system exec cilium-m8wf2 -- cilium status
# 是否启用外部的KV存储
KVStore:                Ok   Disabled
# K8S状态
Kubernetes:             Ok   1.20 (v1.20.5) [linux/amd64]
Kubernetes APIs:        ["cilium/v2::CiliumClusterwideNetworkPolicy", "cilium/v2::CiliumEndpoint", "cilium/v2::CiliumNetworkPolicy", "cilium/v2::CiliumNode", "core/v1::Namespace", "core/v1::Node", "core/v1::Pods", "core/v1::Service", "discovery/v1beta1::EndpointSlice", "networking.k8s.io/v1::NetworkPolicy"]
# kube-proxy replacement工作模式
KubeProxyReplacement:   Strict   [eth0 10.0.3.1 (Direct Routing)]
# cilium-agnet状态
Cilium:                 Ok   1.10.1 (v1.10.1-e6f34c3)
NodeMonitor:            Listening for events on 8 CPUs with 64x4096 of shared memory
Cilium health daemon:   Ok   
# 本节点IP池信息           已分配/总计                分配的节点CIDR
IPAM:                   IPv4: 9/254 allocated from 172.27.2.0/24, 
# 集群网格状态
ClusterMesh:            0/0 clusters ready, 0 global-services
# 带宽管理器             这里提示基于qdisc EDT实现，在eth0上进行带宽管理
BandwidthManager:       EDT with BPF   [eth0]
# 直接路由需要通过宿主机的网络栈
Host Routing:           Legacy
# 基于BPF实现IP遮掩                     不进行遮掩的CIDR
Masquerading:           BPF   [eth0]   172.27.0.0/16 [IPv4: Enabled, IPv6: Disabled]
Controller Status:      55/55 healthy
Proxy Status:           OK, ip 172.27.2.122, 0 redirects active on ports 10000-20000
Hubble:                 Ok   Current/Max Flows: 4095/4095 (100.00%), Flows/s: 67.08   Metrics: Disabled
# 流量加密已禁用
Encryption:             Disabled
# 集群节点/端点状态。如果存在不健康的对象，这里可以看到
Cluster health:         3/3 reachable   (2021-07-02T07:09:23Z)</pre>
<div class="blog_h1"><span class="graybg">开发</span></div>
<div class="blog_h2"><span class="graybg">构建</span></div>
<p>迁出项目后，在项目根目录下执行命令进行开发环境检查：<pre class="crayon-plain-tag">make dev-doctor</pre> 。</p>
<p>为了运行单元测试，需要docker；为了在虚拟机中运行Cilium，需要Vagrant和VirtualBox。建议在虚拟机中进行开发、构建、运行。</p>
<div class="blog_h3"><span class="graybg">Vagrant配置</span></div>
<p>通过下面的命令启动包含Cilium依赖的Vagrant虚拟机：</p>
<pre class="crayon-plain-tag"># 基于base系统cilium/ubuntu
contrib/vagrant/start.sh [vm_name]</pre>
<p>可选的vm_name用于添加新的虚拟机到现有集群中：</p>
<pre class="crayon-plain-tag"># 在节点上构建并安装K8S，主节点k8s1
#     创建一个从节点k8s2
#                使用net-next内核
K8S=1 NWORKERS=1 NETNEXT=1 ./contrib/vagrant/start.sh k8s2+

# 其它环境变量

# 执行vagrant reload而非vagrant up，用于恢复挂起的虚拟机
RELOAD=1
# 不在虚拟机中构建Cilium，用于快速重启（不去完全重新构建Cilium）
NO_PROVISION=1
# 启用Cilium的IPv4支持
IPV4=1
# 选择容器运行时：docker, containerd, crio
RUNTIME=docker
# 设置代理
VM_SET_PROXY=http://10.0.0.1:8088 
# 重新安装Cilium、K8S等，如果安装过程被打断有用
INSTALL=1
# 在虚拟机中构建Cilium前执行make clean
MAKECLEAN=1
# 不在虚拟机中进行构建，假设开发者先前已经在虚拟机中执行过make build
NO_BUILD=1
# 定义额外的挂载点
# USER_MOUNTS=foo 将宿主机的~/foo挂载为虚拟机的/home/vagrant/foo
# USER_MOUNTS=foo,/tmp/bar=/tmp/bar 额外挂载宿主机的/tmp/bar为虚拟机的/tmp/bar
USER_MOUNTS=
# 设置虚拟机内存,单位MB
VM_MEMORY=4096
# 设置虚拟机CPU数量
VM_CPUS=2</pre>
<p>Vagrantfile会在项目根目录寻找文件 <pre class="crayon-plain-tag">.devvmrc</pre>，如果文件存在且可执行，则VM启动时会执行它。你可以用该文件定制VM。</p>
<p>宿主机上的Cilium代码树不需要手工同步到虚拟机中，该目录默认已经通过VirtualBox NFS共享给虚拟机。</p>
<p>你也可以不使用start.sh脚本，手工启动虚拟机并构建Cilium：</p>
<pre class="crayon-plain-tag">vagrant init cilium/ubuntu
vagrant up
vagrant ssh [...]

go get github.com/cilium/cilium
cd go/src/github.com/cilium/cilium/
# 修改代码后，构建Cilium
make
# 重新安装Cilium
make install

mkdir -p /etc/sysconfig/
cp contrib/systemd/cilium.service /etc/systemd/system/
cp contrib/systemd/cilium-docker.service /etc/systemd/system/
cp contrib/systemd/cilium-consul.service /etc/systemd/system/
cp contrib/systemd/cilium  /etc/sysconfig/cilium
usermod -a -G cilium vagrant
systemctl enable cilium-docker
systemctl restart cilium-docker
systemctl enable cilium-consul
systemctl restart cilium-consul
systemctl enable cilium
# 重新启动新安装的Cilium
systemctl restart cilium

# 冒烟测试，确保Cilium正确启动，和Envoy的集成正常工作
tests/envoy-smoke-test.sh</pre>
<div class="blog_h3"><span class="graybg">构建开发者镜像</span></div>
<p>使用下面的命令，依据本地修改，构建cilium-agnet的镜像：</p>
<pre class="crayon-plain-tag">ARCH=amd64 DOCKER_REGISTRY=docker.gmem.cc DOCKER_DEV_ACCOUNT=cilium DOCKER_IMAGE_TAG=1.10.1 make dev-docker-image</pre>
<p>使用下面的命令，依据本地修改，构建cilium-operator的镜像：</p>
<pre class="crayon-plain-tag">make docker-operator-generic-image
# 类似，针对特定云平台的Operator镜像
make docker-operator-aws-image
make docker-operator-azure-image</pre>
<div class="blog_h2"><span class="graybg">调试</span></div>
<div class="blog_h3"><span class="graybg"> 数据路径代码</span></div>
<p>无法单步跟踪，主要依靠<pre class="crayon-plain-tag">cilium monitor</pre>。当cilium-agent或者某个特定的端点在debug模式下运行的话，Cilium会发送调试信息。</p>
<p>要让cilium-agent运行在debug模式，使用<pre class="crayon-plain-tag">--debug</pre>选项或在在运行时执行<pre class="crayon-plain-tag">cilium config debug=true</pre>。</p>
<p>要让特定端点进入debug模式，执行命令<pre class="crayon-plain-tag">cilium endpoint config ID debug=true</pre>。</p>
<p>使用<pre class="crayon-plain-tag">cilium monitor -v -v</pre>可以显示更多调试信息。</p>
<p>开发eBPF时，常遇到的问题是代码无法载入内核，此时，通过<pre class="crayon-plain-tag">cilium endpoint list</pre>会看到<pre class="crayon-plain-tag">not-ready</pre>状态的端点。你可以利用命令<pre class="crayon-plain-tag">cilium endpoint get</pre>来获取端点的eBPF校验日志。</p>
<p>目录<pre class="crayon-plain-tag">/var/run/cilium/state</pre>下的文件说明Cilium如何建立和管理BPF数据路径。.h文件包含了用于BPF程序编译的头文件配置，以数字为名的目录对应特定端点的状态，包括头文件和BPF二进制文件。</p>
<div class="blog_h3"><span class="graybg">查看eBPF Map</span></div>
<p>eBPF Map状态存放在/sys/fs/bpf/下，工具bpf-map可以用于查看其中的内容。</p>
<div class="blog_h1"><span class="graybg">源码分析(cilium-agent)</span></div>
<div class="blog_h2"><span class="graybg">环境准备</span></div>
<p>K8S中cilium-agent启动时的命令行为：</p>
<pre class="crayon-plain-tag">/usr/bin/cilium-agent --config-dir=/tmp/cilium/config-map</pre>
<p>/tmp/cilium/config-map是一个目录， 每个文件对应ConfigMap cilium-config中的一项。我们在本地调试cilium-agent时，可以将配置项写在YAML中：</p>
<pre class="crayon-plain-tag">auto-direct-node-routes: "true"
bpf-lb-map-max: "65536"
bpf-map-dynamic-size-ratio: "0.0025"
bpf-policy-map-max: "16384"
cilium-endpoint-gc-interval: 5m0s
cluster-id: "27"
cluster-name: k8s
cluster-pool-ipv4-cidr: 172.27.0.0/16
cluster-pool-ipv4-mask-size: "24"
custom-cni-conf: "false"
debug: "true"
disable-cnp-status-updates: "true"
enable-auto-protect-node-port-range: "true"
enable-bandwidth-manager: "true"
enable-bpf-clock-probe: "true"
enable-bpf-masquerade: "true"
enable-endpoint-health-checking: "true"
enable-health-check-nodeport: "true"
enable-health-checking: "true"
enable-host-legacy-routing: "false"
enable-host-reachable-services: "true"
enable-hubble: "true"
enable-ipv4: "true"
enable-ipv4-fragment-tracking: "true"
enable-ipv4-masquerade: "true"
enable-ipv6: "false"
enable-ipv6-masquerade: "true"
enable-l7-proxy: "true"
enable-local-redirect-policy: "false"
enable-policy: default
enable-remote-node-identity: "true"
enable-session-affinity: "true"
enable-well-known-identities: "false"
enable-xt-socket-fallback: "true"
hubble-disable-tls: "false"
hubble-listen-address: :4244
hubble-socket-path: /var/run/cilium/hubble.sock
hubble-tls-cert-file: /var/lib/cilium/tls/hubble/server.crt
hubble-tls-client-ca-files: /var/lib/cilium/tls/hubble/client-ca.crt
hubble-tls-key-file: /var/lib/cilium/tls/hubble/server.key
identity-allocation-mode: crd
install-iptables-rules: "true"
install-no-conntrack-iptables-rules: "false"
ipam: cluster-pool
kube-proxy-replacement: strict
kube-proxy-replacement-healthz-bind-address: ""
monitor-aggregation: medium
monitor-aggregation-flags: all
monitor-aggregation-interval: 5s
native-routing-cidr: 172.27.0.0/16
node-port-bind-protection: "true"
operator-api-serve-addr: 127.0.0.1:9234
preallocate-bpf-maps: "false"
sidecar-istio-proxy-image: cilium/istio_proxy
tunnel: disabled
wait-bpf-mount: "false"</pre>
<p>然后使用<pre class="crayon-plain-tag">--config=ciliumd.yaml</pre>启动cilium-agent。</p>
<div class="blog_h2"><span class="graybg">核心数据结构</span></div>
<div class="blog_h3"><span class="graybg">Endpoint</span></div>
<p>端点表示一个容器或者类似的，能够在L3独立寻址（具有独立IP地址）的网络实体。端点由端点管理器管理。</p>
<p>Cilium中的端点，仅仅<span style="background-color: #c0c0c0;">在当前节点的视角下</span>考虑。</p>
<pre class="crayon-plain-tag">type Endpoint struct {
	owner regeneration.Owner

	// 节点范围内唯一的ID
	ID uint16

	// 端点创建时间
	createdAt time.Time

	// 保护端点状态写入操作
	mutex lock.RWMutex

	// 端点对应容器的名字
	containerName string

	// 端点对应容器的ID
	containerID string

	// 如果端点由Docker管理，该字段填写libnetwork网络ID
	dockerNetworkID string

	// 如果端点由Docker管理，该字段填写libnetwork端点ID
	dockerEndpointID string

	// IPVLAN数据路径下，对应的用于尾调用的BPF Map标识符
	datapathMapID int

	// 宿主机端的，连接到端点的网络接口名。通常是veth
	ifName string

	// 宿主机段网络接口索引
	ifIndex int

	// 端点标签配置
	// FIXME: 该字段应该命名为Label
	OpLabels labels.OpLabels

	// 身份标识版本号，该版本号在端点的身份标识标签变化后会递增
	identityRevision int

	// 端点的出口速率
	bps uint64

	// 端点的MAC地址
	mac mac.MAC

	// 端点的IPv6地址
	IPv6 addressing.CiliumIPv6

	// 端点的IPv4地址
	IPv4 addressing.CiliumIPv4

	// 宿主节点的MAC地址，对于每个端点不一样
	nodeMAC mac.MAC

	// 根据端点标签计算的安全标识
	SecurityIdentity *identity.Identity `json:"SecLabel"`

	// 提示端点是否被Istio注入了Cilium兼容的sidecar proxy：
	// 1. 如果是，该sidecar用于应用L7策略规则
	// 2. 如果否，则节点级别的Cilium Envoy用于应用L7策略规则
	// 
	// 目前仅针对HTTP L7规则，Kafka只能在节点级别Enovy中应用
	hasSidecarProxy bool

	// 数据路径的策略相关的Map，包含对所有策略相关的BPF的引用
	policyMap *policymap.PolicyMap

	// 跟踪policyMap压力的指标
	policyMapPressureGauge *metrics.GaugeWithThreshold

	// 端点的数据路径配置
	Options *option.IntOptions

	// 最后N次端点的状态转换信息
	status *EndpointStatus

	// 当前端点特定的DNS代理规则的集合。cilium-agent重启时能够恢复
	DNSRules restore.DNSRules

	// 为此端点拦截的，依然有效的DNS响应缓存
	DNSHistory *fqdn.DNSCache

	// 已经过期或者从DNSHistory中驱除的DNS IPs，在确认没有连接时用这些IP后会自动删除
	DNSZombies *fqdn.DNSZombieMappings

	// dnsHistoryTrigger is the trigger to write down the ep_config.h to make
	// sure that restores when DNS policy is in there are correct
	dnsHistoryTrigger *trigger.Trigger

	// 端点状态
	state State

	// 最后一次编译和安装的BPF头文件的哈希
	bpfHeaderfileHash string

	// 端点对应的K8S Pod名
	K8sPodName string

	// 端点对应的K8S Namespace
	K8sNamespace string

	// 端点对应的Pod
	pod *slim_corev1.Pod

	// 关联到Pod 的容器端口。基于端口名应用K8S网络策略时需要
	k8sPorts policy.NamedPortMap

	// 限制重复的警告日志
	logLimiter logging.Limiter

	// 跟踪k8sPorts被设置至少一次的情况
	hasK8sMetadata bool

	// 端点当前使用的策略的版本
	policyRevision uint64

	// policyRevisionSignals contains a map of PolicyRevision signals that
	// should be triggered once the policyRevision reaches the wanted wantedRev.
	policyRevisionSignals map[*policySignal]bool

	// 应用到代理的策略修订版
	proxyPolicyRevision uint64

	// 写proxyStatistics用的锁
	proxyStatisticsMutex lock.RWMutex

	proxy EndpointProxy

	// 代理重定向的统计信息，键是 policy.ProxyIDs
	proxyStatistics map[string]*models.ProxyStatistics

	// 端点已经更新的、下一次regenerate时使用的策略修订版
	nextPolicyRevision uint64

	// 当端点选项变化后，是否强制重新计算端点的策略
	forcePolicyCompute bool

	// buildMutex synchronizes builds of individual endpoints and locks out
	// deletion during builds
	buildMutex lock.Mutex

	// logger is a logrus object with fields set to report an endpoints information.
	// This must only be accessed with atomic.LoadPointer/StorePointer.
	// 'mutex' must be Lock()ed to synchronize stores. No lock needs to be held
	// when loading this pointer.
	logger unsafe.Pointer

	// policyLogger is a logrus object with fields set to report an endpoints information.
	// This must only be accessed with atomic LoadPointer/StorePointer.
	// 'mutex' must be Lock()ed to synchronize stores. No lock needs to be held
	// when loading this pointer.
	policyLogger unsafe.Pointer

	// controllers is the list of async controllers syncing the endpoint to
	// other resources
	controllers *controller.Manager

	// realizedRedirects maps the ID of each proxy redirect that has been
	// successfully added into a proxy for this endpoint, to the redirect's
	// proxy port number.
	// You must hold Endpoint.mutex to read or write it.
	realizedRedirects map[string]uint16

	// ctCleaned indicates whether the conntrack table has already been
	// cleaned when this endpoint was first created
	ctCleaned bool

	hasBPFProgram chan struct{}

	// selectorPolicy represents a reference to the shared SelectorPolicy
	// for all endpoints that have the same Identity.
	selectorPolicy policy.SelectorPolicy

	desiredPolicy *policy.EndpointPolicy

	realizedPolicy *policy.EndpointPolicy

	visibilityPolicy *policy.VisibilityPolicy

	eventQueue *eventqueue.EventQueue

	// skippedRegenerationLevel is the DatapathRegenerationLevel of the regeneration event that
	// was skipped due to another regeneration event already being queued, as indicated by
	// state. A lower-level current regeneration is bumped to this level to cover for the
	// skipped regeneration levels.
	skippedRegenerationLevel regeneration.DatapathRegenerationLevel

	// DatapathConfiguration is the endpoint's datapath configuration as
	// passed in via the plugin that created the endpoint, e.g. the CNI
	// plugin which performed the plumbing will enable certain datapath
	// features according to the mode selected.
	DatapathConfiguration models.EndpointDatapathConfiguration

	aliveCtx        context.Context
	aliveCancel     context.CancelFunc
	regenFailedChan chan struct{}

	allocator cache.IdentityAllocator

	isHost bool

	noTrackPort uint16
}</pre>
<p>&nbsp;</p>
<div class="blog_h2"><span class="graybg">入口点</span></div>
<p>程序入口点位于daemon/cmd/daemon_main.go文件的RootCmd中： </p>
<pre class="crayon-plain-tag">var (
	log = logging.DefaultLogger.WithField(logfields.LogSubsys, daemonSubsys)

	bootstrapTimestamp = time.Now()

	// RootCmd represents the base command when called without any subcommands
	RootCmd = &amp;cobra.Command{
		Use:   "cilium-agent",
		Short: "Run the cilium agent",
		Run: func(cmd *cobra.Command, args []string) {
			cmdRefDir := viper.GetString(option.CMDRef)
			if cmdRefDir != "" {
				genMarkdown(cmd, cmdRefDir)
				os.Exit(0)
			}

			// gops监听套接字，gops能够获取Go进程的网络连接、调用栈等诊断信息
			addr := fmt.Sprintf("127.0.0.1:%d", viper.GetInt(option.GopsPort))
			addrField := logrus.Fields{"address": addr}
			if err := gops.Listen(gops.Options{
				Addr:                   addr,
				ReuseSocketAddrAndPort: true,
			}); err != nil {
				log.WithError(err).WithFields(addrField).Fatal("Cannot start gops server")
			}
			log.WithFields(addrField).Info("Started gops server")

			bootstrapStats.earlyInit.Start()
			// 环境初始化
			initEnv(cmd)
			bootstrapStats.earlyInit.End(true)
			// 运行cilium-agent
			runDaemon()
		},
	}

	bootstrapStats = bootstrapStatistics{}
)</pre>
<div class="blog_h2"><span class="graybg">环境初始化</span></div>
<p>该部分的整体逻辑包括：</p>
<ol>
<li>初始化配置option.Config
<ol>
<li>初始化Map尺寸（sizeof***Element）相关配置</li>
<li>利用viper读取命令行选项、配置文件中的的选项</li>
<li>配置K8S API Server客户端参数</li>
<li>各种调试选项：flow/envoy/datapath/policy</li>
<li>其它选项处理</li>
</ol>
</li>
<li>打印Logo和版本信息</li>
<li>确然当前用户具有root权限</li>
<li>检查PATH下cilium-envoy文件的版本</li>
<li>如果identity-allocation-mode=crd，断言启用了K8S集成</li>
<li>如果必要，启用PProf端口</li>
<li>如果必要，打开自动BPF Map分配开关</li>
<li>检查目录：
<ol>
<li>LibDir: "/var/lib/cilium"</li>
<li>RunDir: "/var/run/cilium"</li>
<li>BpfDir: "/var/lib/cilium/bpf"</li>
<li>StateDir: "/var/run/cilium/state"</li>
</ol>
</li>
<li>检查数据路径最小要求
<ol>
<li>Linux版本最低4.8.0</li>
<li>需要启用策略路由支持，检查内核配置CONFIG_IP_MULTIPLE_TABLES</li>
<li>如果Cilium启用了IPv6，断言路径/proc/net/if_inet6存在</li>
<li>断言clang、llc存在且版本满足需求。需要在运行时编译BPF源码</li>
<li>如果BpfDir不存在，提示make install-bpf拷贝BPF源码</li>
<li>启动probes.NewProbeManager()探测系统的BPF特性，检查内核参数。断言包linux-tools-generic已经安装</li>
</ol>
</li>
<li>检查或挂载BPF文件系统</li>
<li>检查或挂载Cgroups2文件系统</li>
</ol>
<div class="blog_h3"><span class="graybg">检查用户权限</span></div>
<p>直接要求当前用户为root：</p>
<pre class="crayon-plain-tag">func RequireRootPrivilege(cmd string) {
	if os.Getuid() != 0 {
		fmt.Fprintf(os.Stderr, "Please run %q command(s) with root privileges.\n", cmd)
		os.Exit(1)
	}
}</pre>
<div class="blog_h3"><span class="graybg">检查数据路径最小要求</span></div>
<p>解析内核版本major.minor.patch的代码：</p>
<pre class="crayon-plain-tag">func parseKernelVersion(ver string) (semver.Version, error) {
	verStrs := strings.Split(ver, ".")
	switch {
	case len(verStrs) &lt; 2:
		return semver.Version{}, fmt.Errorf("unable to get kernel version from %q", ver)
	case len(verStrs) &lt; 3:
		verStrs = append(verStrs, "0")
	}
	// We are assuming the kernel version will be something as:
	// 4.9.17-040917-generic

	// If verStrs is []string{ "4", "9", "17-040917-generic" }
	// then we need to retrieve patch number.
	patch := regexp.MustCompilePOSIX(`^[0-9]+`).FindString(verStrs[2])
	if patch == "" {
		verStrs[2] = "0"
	} else {
		verStrs[2] = patch
	}
	return versioncheck.Version(strings.Join(verStrs[:3], "."))
}

// GetKernelVersion returns the version of the Linux kernel running on this host.
func GetKernelVersion() (semver.Version, error) {
	var unameBuf unix.Utsname
	if err := unix.Uname(&amp;unameBuf); err != nil {
		return semver.Version{}, err
	}
	return parseKernelVersion(string(unameBuf.Release[:]))
}</pre>
<p>比较版本时使用库<pre class="crayon-plain-tag">github.com/blang/semver/v4</pre>：</p>
<pre class="crayon-plain-tag">type Range func(Version) bool;  // 这是一个函数，调用后直接判断版本是否符合要求
func MustCompile(constraint string) semver.Range {
	verCheck, err := Compile(constraint)
	if err != nil {
		panic(fmt.Errorf("cannot compile go-version constraint '%s' %s", constraint, err))
	}
	return verCheck
}

minKernelVer = "4.8.0"
isMinKernelVer = versioncheck.MustCompile("&gt;=" + minKernelVer)
if !isMinKernelVer(kernelVersion) {

}</pre>
<p>检查是否支持策略路由：</p>
<pre class="crayon-plain-tag">_, err = netlink.RuleList(netlink.FAMILY_V4)
//                该errno表示地址族不支持？  https://man7.org/linux/man-pages/man3/errno.3.html
if errors.Is(err, unix.EAFNOSUPPORT) {
	log.WithError(err).Error("Policy routing:NOT OK. " +
		"Please enable kernel configuration item CONFIG_IP_MULTIPLE_TABLES")
}</pre>
<p>检查是否支持IPv6：</p>
<pre class="crayon-plain-tag">if option.Config.EnableIPv6 {
	if _, err := os.Stat("/proc/net/if_inet6"); os.IsNotExist(err) {
		log.Fatalf("kernel: ipv6 is enabled in agent but ipv6 is either disabled or not compiled in the kernel")
	}
}</pre>
<p>查找clang二进制文件：</p>
<pre class="crayon-plain-tag">if filePath, err := exec.LookPath("clang"); err != nil {}</pre>
<p>调用bpftool进行特性探测：</p>
<pre class="crayon-plain-tag">probeManager := probes.NewProbeManager()
func NewProbeManager() *ProbeManager {
	newProbeManager := func() {
		probeManager = &amp;ProbeManager{}
		// 调用bpftool -j feature probe探测内核配置
		probeManager.features = probeManager.Probe()
	}
	// Do只会调用一次，这保证了全局变量probeManager不会被重复初始化
	once.Do(newProbeManager)
	return probeManager
}

//  判断Cilium必须的、可选的内核配置是否满足
if err := probeManager.SystemConfigProbes(); err != nil {
	errMsg := "BPF system config check: NOT OK."
	// TODO(brb) warn after GH#14314 has been resolved
	if !errors.Is(err, probes.ErrKernelConfigNotFound) {
		log.WithError(err).Warn(errMsg)
	}
}
func (p *ProbeManager) SystemConfigProbes() error {
	if !p.KernelConfigAvailable() {
		return ErrKernelConfigNotFound
	}
	requiredParams := p.GetRequiredConfig()
	for param, kernelOption := range requiredParams {
		if !kernelOption.Enabled {
			// err
		}
	}
	optionalParams := p.GetOptionalConfig()
	for param, kernelOption := range optionalParams {
		if !kernelOption.Enabled {
			// warn
		}
	}
	return nil
}
// 必须内核配置列表，大部分取决于cilium-agent的选项
func (p *ProbeManager) GetRequiredConfig() map[KernelParam]kernelOption {
	config := p.features.SystemConfig
	coreInfraDescription := "Essential eBPF infrastructure"
	kernelParams := make(map[KernelParam]kernelOption)

	kernelParams["CONFIG_BPF"] = kernelOption{
		Enabled:     config.ConfigBpf.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}
	kernelParams["CONFIG_BPF_SYSCALL"] = kernelOption{
		Enabled:     config.ConfigBpfSyscall.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}
	kernelParams["CONFIG_NET_SCH_INGRESS"] = kernelOption{
		Enabled:     config.ConfigNetSchIngress.Enabled() || config.ConfigNetSchIngress.Module(),
		Description: coreInfraDescription,
		CanBeModule: true,
	}
	kernelParams["CONFIG_NET_CLS_BPF"] = kernelOption{
		Enabled:     config.ConfigNetClsBpf.Enabled() || config.ConfigNetClsBpf.Module(),
		Description: coreInfraDescription,
		CanBeModule: true,
	}
	kernelParams["CONFIG_NET_CLS_ACT"] = kernelOption{
		Enabled:     config.ConfigNetClsAct.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}
	kernelParams["CONFIG_BPF_JIT"] = kernelOption{
		Enabled:     config.ConfigBpfJit.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}
	kernelParams["CONFIG_HAVE_EBPF_JIT"] = kernelOption{
		Enabled:     config.ConfigHaveEbpfJit.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}

	return kernelParams
}
// 可选内核配置列表
func (p *ProbeManager) GetOptionalConfig() map[KernelParam]kernelOption {
	config := p.features.SystemConfig
	kernelParams := make(map[KernelParam]kernelOption)

	kernelParams["CONFIG_CGROUP_BPF"] = kernelOption{
		Enabled:     config.ConfigCgroupBpf.Enabled(),
		Description: "Host Reachable Services and Sockmap optimization",
		CanBeModule: false,
	}
	kernelParams["CONFIG_LWTUNNEL_BPF"] = kernelOption{
		Enabled:     config.ConfigLwtunnelBpf.Enabled(),
		Description: "Lightweight Tunnel hook for IP-in-IP encapsulation",
		CanBeModule: false,
	}
	kernelParams["CONFIG_BPF_EVENTS"] = kernelOption{
		Enabled:     config.ConfigBpfEvents.Enabled(),
		Description: "Visibility and congestion management with datapath",
		CanBeModule: false,
	}

	return kernelParams
}

// 创建一个头文件 /var/run/cilium/state/globals/bpf_features.h 包含描述内核特性的宏
if err := probeManager.CreateHeadersFile(); err != nil {
	log.WithError(err).Fatal("BPF check: NOT OK.")
}
func (p *ProbeManager) CreateHeadersFile() error {
	// ...
	return p.writeHeaders(featuresFile);
}</pre>
<div class="blog_h3"><span class="graybg">挂载文件系统 </span></div>
<p>Cilium可能不在初始命名空间下运行，并且初始命名空间下的/sys/fs/bpf已经被挂载到命名空间的特定位置（option.Config.BPFRoot）。下面的调用检查BPF文件系统是否已经挂载，如果没有则挂载之：</p>
<pre class="crayon-plain-tag">func checkOrMountFS(bpfRoot string, printWarning bool) error {
	// 如果必要，进行挂载
	if bpfRoot == "" {
		checkOrMountDefaultLocations(printWarning)
	} else {
		checkOrMountCustomLocation(bpfRoot, printWarning)
	}
	// 确保没有重复挂载
	multipleMounts, err := hasMultipleMounts()
	if multipleMounts {
		return fmt.Errorf("multiple mount points detected at %s", mapRoot)
	}
	return nil
}

func checkOrMountDefaultLocations(printWarning bool) error {
	// 首先检查 /sys/fs/bpf 是否挂载了BPFFS
	mounted, bpffsInstance, err := mountinfo.IsMountFS(mountinfo.FilesystemTypeBPFFS, mapRoot)

	// 不是挂载点，则这里进行挂载
	if !mounted {
		mountFS(printWarning)
		return nil
	}
	// 挂载了，但是不是BPFFS。这意味着Cilium在容器中运行且宿主机/sys/fs/bpf没有挂载 （要避免这种情况！）
	// 这种情况下使用备用挂载点  /run/cilium/bpffs。此备用挂载点能够被Cilium使用
	// 但是会在Pod重启的时候导致umount，进而导致BPF Map（例如ct表）不可用，后果是
	// 所有到本地容器的连接被丢弃
	//
	//
	if !bpffsInstance {
		setMapRoot(defaults.DefaultMapRootFallback)

		cMounted, cBpffsInstance, err := mountinfo.IsMountFS(mountinfo.FilesystemTypeBPFFS, mapRoot)
		if !cMounted {
			if err := mountFS(printWarning); err != nil {
				return err
			}
		} else if !cBpffsInstance {
			log.Fatalf("%s is mounted but has a different filesystem than BPFFS", defaults.DefaultMapRootFallback)
		}
	}
	log.Infof("Detected mounted BPF filesystem at %s", mapRoot)
	return nil
}
func IsMountFS(mntType int64, path string) (bool, bool, error) {
	var st, pst unix.Stat_t
	// 类似于stat，但当path是符号连接的时候，查看的是链接自身（而非目标）的信息
	err := unix.Lstat(path, &amp;st)
	if err != nil {
		if errors.Is(err, unix.ENOENT) {
			// non-existent path can't be a mount point
			return false, false, nil
		}
		return false, false, &amp;os.PathError{Op: "lstat", Path: path, Err: err}
	}

	parent := filepath.Dir(path)
	err = unix.Lstat(parent, &amp;pst)
	if err != nil {
		return false, false, &amp;os.PathError{Op: "lstat", Path: parent, Err: err}
	}
	if st.Dev == pst.Dev {
		// 如果路径和父目录的设备一样，意味着它不是挂载点
		return false, false, nil
	}

	// 否则，获取文件系统信息
	fst := unix.Statfs_t{}
	err = unix.Statfs(path, &amp;fst)
	if err != nil {
		return true, false, &amp;os.PathError{Op: "statfs", Path: path, Err: err}
	}
	//           文件系统类型
	return true, fst.Type == mntType, nil

}
func mountFS(printWarning bool) error {
	// ...
	if err := unix.Mount(mapRoot, mapRoot, "bpf", 0, ""); err != nil {
		return fmt.Errorf("failed to mount %s: %s", mapRoot, err)
	}
	return nil
}

func hasMultipleMounts() (bool, error) {
	num := 0
	mountInfos, err := mountinfo.GetMountInfo()
	for _, mountInfo := range mountInfos {
		// 什么时候两个条目具有相同挂载点
		if mountInfo.Root == "/" &amp;&amp; mountInfo.MountPoint == mapRoot {
			num++
		}
	}
	return num &gt; 1, nil
}
// 读取/proc/self/mountinfo获取所有挂载信息
func GetMountInfo() ([]*MountInfo, error) {
	fMounts, err := os.Open(mountInfoFilepath)
	defer fMounts.Close()
	return parseMountInfoFile(fMounts)
}</pre>
<p>除了bpf，还需要检查/挂载cgroup2。和bpf不一样的是，存在多个cgroupv2的root mount是无害的，因此不会作重复挂载检查。</p>
<pre class="crayon-plain-tag">func CheckOrMountCgrpFS(mapRoot string) {
	cgrpMountOnce.Do(func() {
		if mapRoot == "" {
			mapRoot = cgroupRoot
		}
		cgrpCheckOrMountLocation(mapRoot)
	})
}
func cgrpCheckOrMountLocation(cgroupRoot string) error {
	setCgroupRoot(cgroupRoot)
	mounted, cgroupInstance, err := mountinfo.IsMountFS(mountinfo.FilesystemTypeCgroup2, cgroupRoot)
	if !mounted {
		return mountCgroup()
	} else if !cgroupInstance {
		return fmt.Errorf("Mount in the custom directory %s has a different filesystem than cgroup2", cgroupRoot)
	}
	return nil
}
func mountCgroup() error {
	unix.Mount("none", cgroupRoot, "cgroup2", 0, "") //...
}</pre>
<div class="blog_h3"><span class="graybg">其它选项处理</span></div>
<p>initEnv阶段会进行大量的选项校验、处理。这里列出一些比较重要的。</p>
<p>至少启用IPv4/IPv6之一：</p>
<pre class="crayon-plain-tag">if !option.Config.EnableIPv4 &amp;&amp; !option.Config.EnableIPv6 {
		log.Fatal("Either IPv4 or IPv6 addressing must be enabled")
	}</pre>
<p>和数据路径模式相关的判断逻辑：</p>
<pre class="crayon-plain-tag">switch option.Config.DatapathMode {
	case datapathOption.DatapathModeVeth:
		// 使用VETH数据路径时，不能配置IPVLAN master设备名，默认使用隧道模式，隧道默认使用VXLAN技术
		if name := viper.GetString(option.IpvlanMasterDevice); name != "undefined" {
			log.WithField(logfields.IpvlanMasterDevice, name).
				Fatal("ipvlan master device cannot be set in the 'veth' datapath mode")
		}
		if option.Config.Tunnel == "" {
			option.Config.Tunnel = option.TunnelVXLAN
		}
	case datapathOption.DatapathModeIpvlan:
		// 使用IPVLAN数据路径时，必须禁用隧道，不支持IPSec
		if option.Config.Tunnel != "" &amp;&amp; option.Config.Tunnel != option.TunnelDisabled {
			log.WithField(logfields.Tunnel, option.Config.Tunnel).
				Fatal("tunnel cannot be set in the 'ipvlan' datapath mode")
		}
		if len(option.Config.Devices) != 0 {
			log.WithField(logfields.Devices, option.Config.Devices).
				Fatal("device cannot be set in the 'ipvlan' datapath mode")
		}
		if option.Config.EnableIPSec {
			log.Fatal("Currently ipsec cannot be used in the 'ipvlan' datapath mode.")
		}

		option.Config.Tunnel = option.TunnelDisabled
		// 尽管IPVLAN模式不允许指定--device，但是后续逻辑都是通过option.Config.Devices保存设备名的，因此这里
		// 读取--ipvlan-master-device并存放在option.Config.Devices
		iface := viper.GetString(option.IpvlanMasterDevice)
		if iface == "undefined" {
			// 必须指定Master设备名
			log.WithField(logfields.IpvlanMasterDevice, option.Config.Devices[0]).
				Fatal("ipvlan master device must be specified in the 'ipvlan' datapath mode")
		}
		option.Config.Devices = []string{iface}
		link, err := netlink.LinkByName(option.Config.Devices[0])
		if err != nil {
			log.WithError(err).WithField(logfields.IpvlanMasterDevice, option.Config.Devices[0]).
				Fatal("Cannot find device interface")
		}
		option.Config.Ipvlan.MasterDeviceIndex = link.Attrs().Index
		option.Config.Ipvlan.OperationMode = connector.OperationModeL3
		if option.Config.InstallIptRules {
			option.Config.Ipvlan.OperationMode = connector.OperationModeL3S
		} else {
			log.WithFields(logrus.Fields{
				logfields.URL: "https://github.com/cilium/cilium/issues/12879",
			}).Warn("IPtables rule configuration has been disabled. This may affect policy and forwarding, see the URL for more details.")
		}
	case datapathOption.DatapathModeLBOnly:
		// 仅LB模式
		log.Info("Running in LB-only mode")
		option.Config.LoadBalancerPMTUDiscovery =
			option.Config.NodePortAcceleration != option.NodePortAccelerationDisabled
		option.Config.KubeProxyReplacement = option.KubeProxyReplacementPartial
		option.Config.EnableHostReachableServices = true
		option.Config.EnableHostPort = false
		option.Config.EnableNodePort = true
		option.Config.EnableExternalIPs = true
		option.Config.Tunnel = option.TunnelDisabled
		option.Config.EnableHealthChecking = false
		option.Config.EnableIPv4Masquerade = false
		option.Config.EnableIPv6Masquerade = false
		option.Config.InstallIptRules = false
		option.Config.EnableL7Proxy = false
	default:
		log.WithField(logfields.DatapathMode, option.Config.DatapathMode).Fatal("Invalid datapath mode")
	}</pre>
<p> 要支持L7策略，必须允许安装iptables规则：</p>
<pre class="crayon-plain-tag">if option.Config.EnableL7Proxy &amp;&amp; !option.Config.InstallIptRules {
		log.Fatal("L7 proxy requires iptables rules (--install-iptables-rules=\"true\")")
	}</pre>
<p>IPSec + 隧道组合，需要4.19+内核：</p>
<pre class="crayon-plain-tag">if option.Config.EnableIPSec &amp;&amp; option.Config.Tunnel != option.TunnelDisabled {
		if err := ipsec.ProbeXfrmStateOutputMask(); err != nil {
			log.WithError(err).Fatal("IPSec with tunneling requires support for xfrm state output masks (Linux 4.19 or later).")
		}
	}</pre>
<p>如果要求Cilium安装iptables规则install-no-conntrack-iptables-rules，让所有Pod流量跳过netfilter的conntrack，则必须使用直接路由模式。因为隧道模式下，外层封包已经自动跳过conntrack：</p>
<pre class="crayon-plain-tag">if option.Config.InstallNoConntrackIptRules {
		if option.Config.Tunnel != option.TunnelDisabled {
			log.Fatalf("%s requires the agent to run in direct routing mode.", option.InstallNoConntrackIptRules)
		}

		// 此外，跳过conntrack必须和IPv4一起使用，原因是用于匹配PodCIDR的是native routing CIDR
		// 此CIDR目前仅支持IPv4
		if !option.Config.EnableIPv4 {
			log.Fatalf("%s requires IPv4 support.", option.InstallNoConntrackIptRules)
		}
	}</pre>
<p>使用隧道时，不能使用直接路由相关的选项。</p>
<p>auto-direct-node-routes：（L2模式下）将直接路由通过给其它节点</p>
<pre class="crayon-plain-tag">if option.Config.Tunnel != option.TunnelDisabled &amp;&amp; option.Config.EnableAutoDirectRouting {
		log.Fatalf("%s cannot be used with tunneling. Packets must be routed through the tunnel device.", option.EnableAutoDirectRoutingName)
	}</pre>
<div class="blog_h2"><span class="graybg">运行cilium-agent</span></div>
<div class="blog_h3"><span class="graybg">整体逻辑</span></div>
<p>runDaemon</p>
<p style="padding-left: 30px;">enableIPForwarding  启用IPv4/IPv6转发</p>
<p style="padding-left: 30px;">iptablesManager.Init  初始化iptables管理器，检查相关内核模块是否可用</p>
<p style="padding-left: 30px;">k8s.Init  初始化各种K8S客户端对象</p>
<p style="padding-left: 30px;">NewDaemon 创建新的cilium-agent守护进程实例</p>
<p style="padding-left: 60px;">WithDefaultEndpointManager 创建端点管理器，负责从底层数据源同步端点信息</p>
<div class="blog_h3"><span class="graybg"> 设置宿主机设备</span></div>
<pre class="crayon-plain-tag">func runDaemon() {
	datapathConfig := linuxdatapath.DatapathConfiguration{
		HostDevice: option.Config.HostDevice,
}</pre>
<p>使用veth数据路径时，默认使用的宿主机设备是cilium_host，它是一个veth，对端也在初始命名空间中，是cilium_net。 </p>
<p>直接路由模式下，从路由上看，发往PodCIDR的流量都是从cilium_host出去：</p>
<pre class="crayon-plain-tag">172.27.2.0      172.27.2.122    255.255.255.0   UG    0      0        0 cilium_host
172.27.2.122    0.0.0.0         255.255.255.255 UH    0      0        0 cilium_host</pre>
<p>但是却找不到什么规则将其发送给Pod的veth（位于初始命名空间的一端）也就是lxc***网卡。</p>
<p>实际上，转发操作是通过挂钩在cilium_host中的BPF程序进行redirect实现的。类似的难以用传统Linux网络拓扑思维难以理解的地方会有很多。</p>
<div class="blog_h3"><span class="graybg">启用IP转发</span></div>
<pre class="crayon-plain-tag">if err := enableIPForwarding(); err != nil {
		log.WithError(err).Fatal("Error when enabling sysctl parameters")
	}

// ...
func enableIPForwarding() error {
	if err := sysctl.Enable("net.ipv4.ip_forward"); err != nil {
		return err
	}
	if err := sysctl.Enable("net.ipv4.conf.all.forwarding"); err != nil {
		return err
	}
	if option.Config.EnableIPv6 {
		if err := sysctl.Enable("net.ipv6.conf.all.forwarding"); err != nil {
			return err
		}
	}
	return nil
}</pre>
<div class="blog_h3"><span class="graybg">初始化iptables管理器</span></div>
<pre class="crayon-plain-tag">iptablesManager := &amp;iptables.IptablesManager{}
	iptablesManager.Init()

// ...

unc (m *IptablesManager) Init() {
	modulesManager := &amp;modules.ModulesManager{}
	haveIp6tables := true
	// 内核模块管理器，读取/proc/modules中的模块信息
	modulesManager.Init()
	// 确保模块加载
	modulesManager.FindOrLoadModules(
		"ip_tables", "iptable_nat", "iptable_mangle", "iptable_raw",
		"iptable_filter");
	modulesManager.FindOrLoadModules(
		"ip6_tables", "ip6table_mangle", "ip6table_raw", "ip6table_filter")

	if err := modulesManager.FindOrLoadModules("xt_socket"); err != nil {
		if option.Config.Tunnel == option.TunnelDisabled {
			// xt_socket执行一个local socket match(根据封包进行套接字查找，匹配应该本地处理的封包)，并且
			// 设置skb mark，这样封包将被Cilium的策略路由导向本地网络栈，不会被ip_forward()处理
			//
			// 如果xt_socket模块不存在，那么可以禁用ip_early_demux，避免在ip_forward()中明确的drop
			// 在隧道模式下可以不需要ip_early_demux，因为我们可以在BPF逻辑中设置skb mark，这个BPF也是
			// 早于策略路由阶段的，可以保证封包被导向本地网络栈，不会ip_forward()转发
			// 
			// 如果对于任何场景，我们都能保证在封包到达策略路由之前，被设置好“to proxy” 这个skb mark
			// 则可以不需要xt_socket模块。目前对于endpoint routing mode，无法满足这一点
			log.WithError(err).Warning("xt_socket kernel module could not be loaded")

			if option.Config.EnableXTSocketFallback {
				v4disabled := true
				v6disabled := true
				if option.Config.EnableIPv4 {
					v4disabled = sysctl.Disable("net.ipv4.ip_early_demux") == nil
				}
				if option.Config.EnableIPv6 {
					v6disabled = sysctl.Disable("net.ipv6.ip_early_demux") == nil
				}
				if v4disabled &amp;&amp; v6disabled {
					m.ipEarlyDemuxDisabled = true
					log.Warning("Disabled ip_early_demux to allow proxy redirection with 
                                               original source/destination address without xt_socket support 
                                               also in non-tunneled datapath modes.")
				} else {
					log.WithError(err).Warning("Could not disable ip_early_demux, traffic
                                           redirected due to an HTTP policy or visibility may be dropped unexpectedly")
				}
			}
		}
	} else {
		m.haveSocketMatch = true
	}
	m.haveBPFSocketAssign = option.Config.EnableBPFTProxy

	v, err := ip4tables.getVersion()
	if err == nil {
		switch {
		case isWaitSecondsMinVersion(v):
			m.waitArgs = []string{waitString, fmt.Sprintf("%d", option.Config.IPTablesLockTimeout/time.Second)}
		case isWaitMinVersion(v):
			m.waitArgs = []string{waitString}
		}
	}
}</pre>
<p>封包到达内核之后，内核需要知道如何路由它。为了避免反复查找路由表，Linux出现过很多路由缓存机制。从3.6开始，Linux废弃了全局路由缓存，由各子系统（例如TCP协议栈）负责维护路由缓存。</p>
<p>在套接字级别，有一个dst字段作为路由缓存。当一个TCP连接最初建立时，此字段为空，随后dst被填充，该套接字的生命周期内，后续到来的skb都不再需要查找路由。</p>
<p>ip_early_demux是一个内核特性。它向（网络栈的）下优化某些类型的本地套接字（目前是已建立的TCP套接字）输入封包的处理，它的工作原理是：</p>
<ol>
<li>内核接收到封包后，需要查找skb对应的路由，然后查找skb对应的socket</li>
<li>这里存在一种浪费，对应已建立的连接，属于同一socket的skb的路由是一致的</li>
<li>如果能将路由信息缓存到套接字上，那么就可以避免为每个skb查找路由</li>
</ol>
<p>问题在于，对于主要（60%+流量）作为路由器使用的Linux系统，它会引入不必要的吞吐量下降，可以禁用。</p>
<p>回到Cilium的场景，使用HTTP策略时，需要将流量重定向给Envoy，同时保持源、目的IP地址不变。这个重定向是依赖于xt_socket模块的。如果xt_socket不可用则需要禁用ip_early_demux，否则会导致原本应该发给Envoy的流量被转发走。</p>
<div class="blog_h3"><span class="graybg">初始化K8S</span></div>
<pre class="crayon-plain-tag">func Init(conf k8sconfig.Configuration) error {
	// 创建K8S核心API客户端
	k8sRestClient, closeAllDefaultClientConns, err := createDefaultClient()
	if err != nil {
		return fmt.Errorf("unable to create k8s client: %s", err)
	}
	// 创建Cilium客户端
	closeAllCiliumClientConns, err := createDefaultCiliumClient()
	if err != nil {
		return fmt.Errorf("unable to create cilium k8s client: %s", err)
	}
	// 创建API Extensions客户端
	if err := createAPIExtensionsClient(); err != nil {
		return fmt.Errorf("unable to create k8s apiextensions client: %s", err)
	}

	// 心跳函数
	heartBeat := func(ctx context.Context) error {
		// Kubernetes默认的心跳是获取所在节点的信息
		// [0] https://github.com/kubernetes/kubernetes/blob/v1.17.3/pkg/kubelet/kubelet_node_status.go#L423
		// 这对于Cilium来说太重，因此这里的心跳采用检查/healthz端点的方式
		res := k8sRestClient.Get().Resource("healthz").Do(ctx)
		return res.Error()
	}
	
	// 对K8S进行心跳检测
	if option.Config.K8sHeartbeatTimeout != 0 {
		controller.NewManager().UpdateController("k8s-heartbeat",
			controller.ControllerParams{
				DoFunc: func(context.Context) error {
					runHeartbeat(
						heartBeat, // 心跳函数
						option.Config.K8sHeartbeatTimeout, // 超时
						// 后面的是超时后的回调，这里是关闭客户端连接
						closeAllDefaultClientConns,
						closeAllCiliumClientConns,
					)
					return nil
				},
				// 心跳运行间隔
				RunInterval: option.Config.K8sHeartbeatTimeout,
			},
		)
	}
	// 获取K8S版本进而推导具有哪些特性
	if err := k8sversion.Update(Client(), conf); err != nil {
		return err
	}
	if !k8sversion.Capabilities().MinimalVersionMet {
		return fmt.Errorf("k8s version (%v) is not meeting the minimal requirement (%v)",
			k8sversion.Version(), k8sversion.MinimalVersionConstraint)
	}

	return nil
}

// 创建Cilium客户端
import 	clientset "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned"
func createDefaultCiliumClient() (func(), error) {
	restConfig, err := CreateConfig()
	closeAllConns := setDialer(restConfig)
	createdCiliumK8sClient, err := clientset.NewForConfig(restConfig)
	k8sCiliumCLI.Interface = createdCiliumK8sClient
	return closeAllConns, nil
}
// 为 rest.Config设置Dial，一个负责拨号（创建TCP连接）的函数
func setDialer(config *rest.Config) func() {
	if option.Config.K8sHeartbeatTimeout == 0 {
		return func() {}
	}
	ctx := (&amp;net.Dialer{
		Timeout:   option.Config.K8sHeartbeatTimeout,
		KeepAlive: option.Config.K8sHeartbeatTimeout,
	}).DialContext
	dialer := connrotation.NewDialer(ctx)
	// 拨号器的DialContext用于创建连接，CloseAll用于关闭它创建的所有连接
	// 这个关闭函数返回，并在心跳失败时调用以关闭连接
	config.Dial = dialer.DialContext
	return dialer.CloseAll
}</pre>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">零散问题</span></div>
<div class="blog_h3"><span class="graybg">native routing cidr must be configured with option --native-routing-cidr in combination with --masquerade --tunnel=disabled</span></div>
<p>禁用隧道，使用直接路由时，必须指定native-routing-cidr选项（对应Helm值nativeRoutingCIDR）。该选项必须设置为PodCIDR，也就是Kubernetes的--cluster-cidr。该选项提示Cilium直接路由的目的地址范围。</p>
<div class="blog_h3"><span class="graybg">Failed to compile XDP program" error="Failed to load prog with ip cilium</span></div>
<p>可能提示不支持XDP，禁用<pre class="crayon-plain-tag">--set loadBalancer.acceleration=disabled</pre>。</p>
<div class="blog_h2"><span class="graybg">cilium status提示端点不通</span></div>
<p>异常时输出示例：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system exec cilium-4bsb6 -- cilium status
# ...
# Cluster health:           0/3 reachable   (2021-07-02T04:06:04Z)
#   Name                    IP              Node          Endpoints
#   k8s/k8s-3 (localhost)   10.0.3.3        reachable     unreachable
#   k8s/k8s-1               10.0.3.1        reachable     unreachable
#   k8s/k8s-2               10.0.3.2        unreachable   unreachable</pre>
<p>正常时输出示例：</p>
<pre class="crayon-plain-tag"># Cluster health:         3/3 reachable   (2021-07-02T04:09:19Z)</pre>
<p>任何导致网络不通的故障，都会出现类似报错。</p>
<div class="blog_h3"><span class="graybg">禁用隧道但未同步路由</span></div>
<p>配置<pre class="crayon-plain-tag">--set tunnel=disabled</pre>以使用直接路由模式后，容器网络封包不再使用VXLAN包装，而是直接在集群节点之间路由。</p>
<p>这需要集群节点之间有正确的路由规则：</p>
<ol>
<li>如果节点是L2直连的，应当设置<pre class="crayon-plain-tag">--set autoDirectNodeRoutes=true</pre>。这样，路由规则会直接安装到节点路由表中，例如：<br />
<pre class="crayon-plain-tag">route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
# 下面的两个PodCIDR的节点CIDR，通过以太网路由给其它节点
172.27.0.0      10.0.3.2        255.255.255.0   UG    0      0        0 eth0
172.27.1.0      10.0.3.3        255.255.255.0   UG    0      0        0 eth0
# 下面是本节点的CIDR，交给cilium_host虚拟设备处理
172.27.2.0      172.27.2.122    255.255.255.0   UG    0      0        0 cilium_host
172.27.2.122    0.0.0.0         255.255.255.255 UH    0      0        0 cilium_host</pre>
</li>
<li>如果节点是L3互联的，则路由器需要和Cilium进行某种形式的集成。例如通过BGP协议，或者使用crd这个IPAM，静态规定每个节点的CIDR，然后更新路由器的路由规则</li>
</ol>
<div class="blog_h3"><span class="graybg">IaaS层配置异常</span></div>
<p>如果节点是虚拟机，则要考虑IaaS层的网络设置。例如OpenStack可能需要为Port配置Allowed Address Pairs，将PodCIDR加入其中。</p>
<div class="blog_h2"><span class="graybg">DSR模式下无法从集群外部访问LB服务</span></div>
<p>LoadBalancer服务的IP地址为：10.0.11.20，服务端点IP地址为：172.28.1.76。流量入口节点为m1，服务端点所在节点为m3。</p>
<p>在m1上通过tcpdump抓包：</p>
<pre class="crayon-plain-tag"># tcpdump -i any -nnn -vvv 'dst 172.28.1.76'
15:21:02.310338 IP (tos 0x10, ttl 64, id 58773, offset 0, flags [DF], proto TCP (6), length 68, options (unknown 154))
    10.2.0.1.36568 &gt; 172.28.1.76.2379: Flags [S], cksum 0xb799 (incorrect -&gt; 0x65e9), seq 2591998745, win 64240, options [mss 1460,sackOK,TS val 899286115 ecr 0,nop,wscale 9], length 0</pre>
<p>可以看到，在入口节点m1，已经尝试将请求直接转发给位于m3的172.28.1.76.2379。携带了IP选项154。但是，在m3上抓包，没有任何相关信息。</p>
<p>禁用DSR后，两个节点抓包可以发现通信正常：</p>
<pre class="crayon-plain-tag"># m1
15:36:13.301471 IP (tos 0x10, ttl 64, id 47272, offset 0, flags [DF], proto TCP (6), length 57)
    10.2.0.61.38732 &gt; 172.28.1.76.2379: Flags [P.], cksum 0xb7d2 (incorrect -&gt; 0x5309), seq 1086727409:1086727414, ack 3915217720, win 126, options [nop,nop,TS val 900197106 ecr 488619248], length 5

# m3
15:36:13.301825 IP (tos 0x10, ttl 64, id 47272, offset 0, flags [DF], proto TCP (6), length 57)
    10.2.0.61.38732 &gt; 172.28.1.76.2379: Flags [P.], cksum 0xb7d2 (incorrect -&gt; 0x5309), seq 1086727409:1086727414, ack 3915217720, win 126, options [nop,nop,TS val 900197106 ecr 488619248], length 5</pre>
<p>差别在于源地址、IP选项不同。考虑是OpenStack源地址检查的原因。增加Allowed Address Pairs：10.0.0.0/8，问题解决。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/cilium">Cilium学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/cilium/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>如何在Pod中执行宿主机上的命令</title>
		<link>https://blog.gmem.cc/exec-host-cmd-from-within-a-pod</link>
		<comments>https://blog.gmem.cc/exec-host-cmd-from-within-a-pod#comments</comments>
		<pubDate>Tue, 24 Mar 2020 07:05:27 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=29459</guid>
		<description><![CDATA[<p>基础知识回顾 要回答标题中的疑问，我们首先要清楚，Pod是什么？ Pod的翻译叫容器组，顾名思义，是一组容器。叫做“组”是因为这些容器： 总是被同时调度，调度到同一节点 共享网络，具有相同的IP地址和端口空间，可以通过localhost相互访问 可以基于SystemV信号量、POSIX消息队列等方式，进行进程间通信 共享存储卷（需要各自分别挂载） 从效果上看，容器组运行在一个虚拟的“主机”中。这个“主机”基于Linux命名空间、cgroups等机制和宿主机相互隔离。 虽说容器具有隔离性，但是这种隔离程度远远不如虚拟机，容器本质上就是进程。内核也提供了接口，允许你切换命名空间。只需要切换到宿主机的初始（Initial）命名空间，理论上就可以运行宿主机文件系统中的任何程序，并保证程序的行为正常。 Linux命名空间 参考Linux知识集锦 K8S共享命名空间 相关Pod字段 在K8S的Pod的Spec中，和命名空间有关的编排配置包括： [crayon-69e09a47b25a4770890814/] 创建一个启用上述配置，和宿主机共享网络、PID、IPC命名空间的Pod后，通过[crayon-69e09a47b25a8754897927-i/]执行[crayon-69e09a47b25aa819436536-i/]，你会看到宿主机上的进程。 其它User、Mount、Cgroup等几种命名空间，没有相应的配置字段。需要强调的是，Mount命名空间无法共享，容器需要独立的文件系统树来挂载镜像。仅仅通过K8S提供的编排配置，无法实现我们的目标 —— 因为看不到和宿主机一样的文件系统树。 K8S安全配置 特权模式 上文提到过，内核允许切换到某个命名空间，然后执行应用程序。系统调用setns、unshare、clone等提供了切换命名空间的接口，命令行工具nsenter、unshare也可以实现相同的功能。 <a class="read-more" href="https://blog.gmem.cc/exec-host-cmd-from-within-a-pod">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/exec-host-cmd-from-within-a-pod">如何在Pod中执行宿主机上的命令</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">基础知识回顾</span></div>
<p>要回答标题中的疑问，我们首先要清楚，Pod是什么？</p>
<p>Pod的翻译叫容器组，顾名思义，是一组容器。叫做“组”是因为这些容器：</p>
<ol>
<li>总是被同时调度，调度到同一节点</li>
<li>共享网络，具有相同的IP地址和端口空间，可以通过localhost相互访问</li>
<li>可以基于SystemV信号量、POSIX消息队列等方式，进行进程间通信</li>
<li>共享存储卷（需要各自分别挂载）</li>
</ol>
<p>从效果上看，容器组运行在一个虚拟的“主机”中。这个“主机”基于<span style="background-color: #c0c0c0;">Linux命名空间、cgroups等机制</span>和宿主机相互隔离。</p>
<p>虽说容器具有隔离性，但是这种隔离程度远远不如虚拟机，容器本质上就是进程。内核也提供了接口，允许你切换命名空间。只需要切换到宿主机的初始（Initial）命名空间，理论上就可以运行宿主机文件系统中的任何程序，并保证程序的行为正常。</p>
<div class="blog_h1"><span class="graybg">Linux命名空间</span></div>
<p>参考<a href="/linux-faq#namespace">Linux知识集锦</a></p>
<div class="blog_h1"><span class="graybg">K8S共享命名空间</span></div>
<div class="blog_h2"><span class="graybg">相关Pod字段</span></div>
<p>在K8S的Pod的Spec中，和命名空间有关的编排配置包括：</p>
<pre class="crayon-plain-tag">type PodSpec struct {
	// 使用宿主机的网络命名空间
	HostNetwork bool `json:"hostNetwork,omitempty" protobuf:"varint,11,opt,name=hostNetwork"`
	// 使用宿主机的PID命名空间
	HostPID bool `json:"hostPID,omitempty" protobuf:"varint,12,opt,name=hostPID"`
	// 使用宿主机的IPC命名空间
	HostIPC bool `json:"hostIPC,omitempty" protobuf:"varint,13,opt,name=hostIPC"`
	// 让所有容器共享同一个PID命名空间，此选项不能和HostPID同时设置
	// 启用该选项后，容器的第一个进程不会赋予PID 1
	ShareProcessNamespace *bool `json:"shareProcessNamespace,omitempty" protobuf:"varint,27,opt,name=shareProcessNamespace"`
}</pre>
<p>创建一个启用上述配置，和宿主机共享网络、PID、IPC命名空间的Pod后，通过<pre class="crayon-plain-tag">kubectl exec</pre>执行<pre class="crayon-plain-tag">ps aux</pre>，你会看到宿主机上的进程。</p>
<p>其它User、Mount、Cgroup等几种命名空间，没有相应的配置字段。需要强调的是，Mount命名空间无法共享，容器需要独立的文件系统树来挂载镜像。仅仅通过K8S提供的编排配置，无法实现我们的目标 —— 因为看不到和宿主机一样的文件系统树。</p>
<div class="blog_h1"><span class="graybg">K8S安全配置</span></div>
<div class="blog_h2"><span class="graybg">特权模式</span></div>
<p>上文提到过，内核允许切换到某个命名空间，然后执行应用程序。系统调用setns、unshare、clone等提供了切换命名空间的接口，命令行工具nsenter、unshare也可以实现相同的功能。</p>
<p>如果我们在启用上述配置的容器中执行<pre class="crayon-plain-tag">nsenter</pre>，尝试切换到初始Mount命名空间，会提示Permission denied错误：</p>
<pre class="crayon-plain-tag">nsenter -m -t 1                                                                                                                                                       
nsenter: cannot open /proc/1/ns/mnt: Permission denied</pre>
<p>这说明容器没有足够的权限进行操作。</p>
<p>Kubernets允许容器以特权模式运行，你只需要配置安全上下文即可。安全上下文包括Pod、Container两个级别。</p>
<div class="blog_h3"><span class="graybg">Pod安全上下文</span></div>
<pre class="crayon-plain-tag">type PodSpec struct {
	// 提供Pod级别的安全属性，并为容器安全属性提供默认值
	SecurityContext *PodSecurityContext `json:"securityContext,omitempty" protobuf:"bytes,14,opt,name=securityContext"`
}


type PodSecurityContext struct {
	// 应用到所有容器的SELinux上下文，如果不指定，则容器运行时为每个容器指定随机的SELinux上下文
	SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" protobuf:"bytes,1,opt,name=seLinuxOptions"`
	// 运行容器进程入口点使用的UID，默认从镜像元数据中获取UID
	RunAsUser *int64 `json:"runAsUser,omitempty" protobuf:"varint,2,opt,name=runAsUser"`
	// 运行容器进程入口点使用的GID，默认从使用容器运行时的默认值
	RunAsGroup *int64 `json:"runAsGroup,omitempty" protobuf:"varint,6,opt,name=runAsGroup"`
	// 提示容器必须以非Root身份运行，如果设置为true，则Kubelet会在运行时校验镜像
	// 确保它不以UID 0运行，如果发现镜像以UID 0 进行则导致启动失败
	RunAsNonRoot *bool `json:"runAsNonRoot,omitempty" protobuf:"varint,3,opt,name=runAsNonRoot"`
	// 额外的补充组，赋予容器的第一个进程，作为组GID的补充
	SupplementalGroups []int64 `json:"supplementalGroups,omitempty" protobuf:"varint,4,rep,name=supplementalGroups"`
	// 一个特殊的、应用到所有容器的补充组
	// 某些类型的卷，允许Kubelet修改卷的所有者，这可以确保容器有权访问卷的内容
	// 该选项导致：
	// 1. 卷的所有者GID设置为FSGroup
	// 2. setgid位被启用，这导致卷中新创建的文件的所有者为FSGroup
	// 3. 卷中文件的模式和rw-rw----进行或操作，也就是启用所有者、所在组的读写权限

	// 如果不配置，kubelet不会修改任何卷的所有者和文件模式

	FSGroup *int64 `json:"fsGroup,omitempty" protobuf:"varint,5,opt,name=fsGroup"`
	// 指定一系列命名空间化的Sysctl键值
	// 如果容器运行时不支持某个Sysctl则可能导致启动失败
	Sysctls []Sysctl `json:"sysctls,omitempty" protobuf:"bytes,7,rep,name=sysctls"`
}</pre>
<div class="blog_h3"><span class="graybg">容器安全上下文</span></div>
<p>容器安全上下文中，有一部分字段和Pod安全上下文一样，它们会<span style="background-color: #c0c0c0;">覆盖Pod安全上下文</span>中的对应设置。 </p>
<pre class="crayon-plain-tag">type Container struct {
	SecurityContext *SecurityContext `json:"securityContext,omitempty" protobuf:"bytes,15,opt,name=securityContext"`
}


type SecurityContext struct {
	// 需要给容器添加/删除的能力列表，默认能力取决于容器运行时
	Capabilities *Capabilities `json:"capabilities,omitempty" protobuf:"bytes,1,opt,name=capabilities"`
	// 以特权模式运行容器。这种模式下，容器中进程的身份等价于宿主机的root
	Privileged *bool `json:"privileged,omitempty" protobuf:"varint,2,opt,name=privileged"`
	// 覆盖Pod上下文设置
	SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" protobuf:"bytes,3,opt,name=seLinuxOptions"`
	// 覆盖Pod上下文设置
	RunAsUser *int64 `json:"runAsUser,omitempty" protobuf:"varint,4,opt,name=runAsUser"`
	// 覆盖Pod上下文设置
	RunAsGroup *int64 `json:"runAsGroup,omitempty" protobuf:"varint,8,opt,name=runAsGroup"`
	// 覆盖Pod上下文设置
	RunAsNonRoot *bool `json:"runAsNonRoot,omitempty" protobuf:"varint,5,opt,name=runAsNonRoot"`
	// 容器的根文件系统是否设置为只读
	ReadOnlyRootFilesystem *bool `json:"readOnlyRootFilesystem,omitempty" protobuf:"varint,6,opt,name=readOnlyRootFilesystem"`
	// 是否允许子进程获得比父进程更多的特权，控制容器进程的no_new_privs标记是否被设置
	// 如果容器是运行在特权模式，或者具有CAP_SYS_ADMIN能力，则该配置自动为true
	AllowPrivilegeEscalation *bool `json:"allowPrivilegeEscalation,omitempty" protobuf:"varint,7,opt,name=allowPrivilegeEscalation"`
	// 指定该容器的proc挂载类型，默认的
	ProcMount *ProcMountType `json:"procMount,omitempty" protobuf:"bytes,9,opt,name=procMount"`
}</pre>
<div class="blog_h3"><span class="graybg">Privileged </span></div>
<p>要满足我们的需求，只需要设置容器安全上下文的Privileged为True就足够了。这样你就可以通过nsenter进入宿主机的Mount命名空间，并且随意的运行命令了，例如通过systemctl判断某些服务是否正常运行。</p>
<div class="blog_h1"><span class="graybg">K8S完整配置样例</span></div>
<p>这个样例允许我们在容器中访问宿主机的日志、控制宿主机的systemd，而不需要切换整个Mount命名空间。我们目前项目的一个需求就是，能够读取节点的内核日志环、Journald日志，可以用下面这种卷挂载的方式满足：</p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: centos
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: centos
  template:
    metadata:
      labels:
        name: centos
    spec:
      # 加入宿主机网络命名空间
      hostNetwork: true
      # 加入宿主机PID命名空间
      hostPID: true
      # 加入宿主机IPC命名空间
      hostIPC: true 
      containers:
      - image: docker.gmem.cc/centos:7.6
        imagePullPolicy: Always
        name: centos
        securityContext:
          # 设置PID为root
          runAsUser: 0
          # 特权模式
          privileged: true
        volumeMounts:
        # 这个挂载允许容器中的systemctl和宿主机的systemd通信
        - name: dbus
          mountPath: /var/run/dbus
        - name: run-systemd
          mountPath: /run/systemd
        # 这个挂载允许查看宿主机的systemd配置
        - name: etc-systemd
          mountPath: /etc/systemd
        # 这个挂载允许容器读取非journald管理的日志
        - name: var-log
          mountPath: /var/log
        - name: var-run
          mountPath: /var/run
        - name: run
          mountPath: /run
        - name: usr-lib-systemd
          mountPath: /usr/lib/systemd
      volumes:
      - name: dbus
        hostPath:
          path: /var/run/dbus
          type: Directory
      - name: run-systemd
        hostPath:
          path: /run/systemd
          type: Directory
      - name: etc-systemd
        hostPath:
          path: /etc/systemd
          type: Directory
      - name: var-log
        hostPath:
          path: /var/log
          type: Directory
      - name: var-run
        hostPath:
          path: /var/run
          type: Directory
      # /var/run 是 /run的符号链接
      - name: run
        hostPath:
          path: /run
          type: Directory
      - name: usr-lib-systemd
        hostPath:
          path: /usr/lib/systemd
          type: Directory</pre>
<div class="blog_h1"><span class="graybg">Go命名空间编程</span></div>
<div class="blog_h2"><span class="graybg">切换命名空间</span></div>
<div class="blog_h3"><span class="graybg">nsenter</span></div>
<p>这是一个命令行工具，能够在指定的命名空间中执行命令。K8S的<a href="https://github.com/kubernetes/utils/blob/master/nsenter/nsenter.go">utils/nsenter</a>包对该命令进行了封装，可以参考。</p>
<div class="blog_h3"><span class="graybg">setns</span></div>
<p>要编程式的切换命名空间，可以利用这个系统调用。 </p>
<p>在Go语言下，你需要注意的一点是，setns调用<span style="background-color: #c0c0c0;">可能需要单线程上下文</span>。而Go运行时是多线程的，你必须<span style="background-color: #c0c0c0;">在Go运行时启动之前，执行setns调用</span>。要实现这种提前调用，可以利用cgo的constructor技巧，该技巧能够在Go运行时启动之前，执行一个任意的C函数：</p>
<pre class="crayon-plain-tag">/*
__attribute__((constructor)) void init() {
    // 这里的代码会在Go运行时启动前执行
    // 它会在单线程的C上下文中运行
}
*/
import "C"</pre>
<p>libcontainer提供了<a href="https://github.com/opencontainers/runc/tree/master/libcontainer/nsenter">基于此技巧的例子</a>。</p>
<p>在Go语言中，你可以这样设置NS：</p>
<pre class="crayon-plain-tag">// 将当前线程的NS设置为ns.Fd()这个文件描述符所指向的网络命名空间
if err := unix.Setns(int(ns.Fd()), unix.CLONE_NEWNET); err != nil {
	return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err)
} </pre>
<div class="blog_h3"><span class="graybg">在NS中执行函数</span></div>
<pre class="crayon-plain-tag">// 在ns所代表的网络命名空间（以网络命名空间的文件描述符锚定）
func (ns *netNS) Do(toRun func(NetNS) error) error {
	// 如果命名空间（文件描述符）已经关闭
	if err := ns.errorIfClosed(); err != nil {
		return err
	}
	// 设置当前调用者goroutine的网络NS为当前对象所代表的网络OS
	// 然后运行函数，完毕后重置为线程原先的网络命名空间
	// 为了防止goroutine底层的OS线程切换，需要锁定、回调执行完毕后解锁
	containedCall := func(hostNS NetNS) error {
		// 得到当前线程的NS
		threadNS, err := GetCurrentNS()
		if err != nil {
			return fmt.Errorf("failed to open current netns: %v", err)
		}
		// 关闭文件描述符
		defer threadNS.Close()

		// 设置NS
		if err = ns.Set(); err != nil {
			return fmt.Errorf("error switching to ns %v: %v", ns.file.Name(), err)
		}
		// 结束回调后，恢复NS，解锁线程
		defer func() {
			err := threadNS.Set() // switch back
			if err == nil {
				// 仅当前NS切回成功，才解锁线程，否则
				// 保持锁定，这会导致gouroutine结束了销毁OS线程
				// 这种做法可能不是最优解，但是安全，不会出现NS混乱
				runtime.UnlockOSThread()
			}
		}()
		// 调用真实的回调
		return toRun(hostNS)
	}

	// 保存当前命名空间的句柄
	hostNS, err := GetCurrentNS()
	if err != nil {
		return fmt.Errorf("Failed to open current namespace: %v", err)
	}
	// 总是关闭（在回调goroutine完毕后）
	defer hostNS.Close()

	// 用于等待回调goroutine完毕
	var wg sync.WaitGroup
	wg.Add(1)

	// 关键之处：启用一个新的goroutine，在其中执行回调
	// 这样做的原因是，当前goroutine不切换NS，保证了安全性
	// 而新gorouinte会先切NS，再切回去，这过程万一失败，直接抛弃它的底层OS线程即可
	var innerError error
	go func() {
		defer wg.Done()
		// 锁定OS线程，重要
		runtime.LockOSThread()
		innerError = containedCall(hostNS)
	}()
	wg.Wait()

	return innerError
} </pre>
<p>调用上述函数的例子：</p>
<pre class="crayon-plain-tag">err = networkNS.Do(func(ns.NetNS) error {
	var err error
	// 获取命名空间中的网络接口
	lo, err = net.InterfaceByName("lo")
	return err
})</pre>
<div class="blog_h2"><span class="graybg">获取命名空间</span></div>
<div class="blog_h3"><span class="graybg">当前线程命名空间</span></div>
<pre class="crayon-plain-tag">func getCurrentThreadNetNSPath() string {
	// /proc/self/ns/net 返回的是主线程的命名空间
	// 要获得当前goroutine的后备线程的命名空间，使用：
	return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid())
}</pre>
<div class="blog_h2"><span class="graybg">创建命名空间</span></div>
<p>下面的例子演示了如何创建一个不依赖于线程，可独立存在的网络命名空间：</p>
<pre class="crayon-plain-tag">import "golang.org/x/sys/unix"

// 获取网络命名空间目录
func getNsRunDir() string {
	xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR")

	// 如果XDG_RUNTIME_DIR被设置，检查是否当前用户是/var/run的所有者
	// 如果不是，提示当前运行在一个User命名空间中
	// 运行时目录应该取：$XDG_RUNTIME_DIR/netns
	if xdgRuntimeDir != "" {
		if s, err := os.Stat("/var/run"); err == nil {
			// 发起系统调用，获取文件信息
			st, ok := s.Sys().(*syscall.Stat_t)
			if ok &amp;&amp; int(st.Uid) != os.Geteuid() {
				return path.Join(xdgRuntimeDir, "netns")
			}
		}
	}

	return "/var/run/netns"
}

func NewNS() (ns.NetNS, error) {
	nsRunDir := getNsRunDir()
	// 随机目录名
	b := make([]byte, 16)
	_, err := rand.Reader.Read(b)

	// 创建目录，如果它被挂载到了其它命名空间，则必须改为共享挂载点
	err = os.MkdirAll(nsRunDir, 0755)

	// 重新挂载它，设置为共享挂载，MS_REC表示递归的处理子树中的挂载，都改为共享
	// 如果它尚不是一个挂载点，则调用会失败
	err = unix.Mount("", nsRunDir, "none", unix.MS_SHARED|unix.MS_REC, "")
	if err != nil {
		if err != unix.EINVAL {
			return nil, fmt.Errorf("mount --make-rshared %s failed: %q", nsRunDir, err)
		}
		// 重新Bind挂载到它自身，可以“升级”为挂载点，递归处理子树
		err = unix.Mount(nsRunDir, nsRunDir, "none", unix.MS_BIND|unix.MS_REC, "")		
		if err != nil {
			return nil, fmt.Errorf("mount --rbind %s %s failed: %q", nsRunDir, nsRunDir, err)
		}
		// 再次标记为共享挂载点
		err = unix.Mount("", nsRunDir, "none", unix.MS_SHARED|unix.MS_REC, "")
	}

	nsName := fmt.Sprintf("cnitest-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])

	// 在挂载点下创建一个空白文件，获取它的文件描述符
	nsPath := path.Join(nsRunDir, nsName)
	mountPointFd, err := os.Create(nsPath)
	if err != nil {
		return nil, err
	}
	// 关闭文件描述符
	mountPointFd.Close()

	// 确保在出错时挂载点被清理掉
	// 如果命名空间已经成功挂载，则该调用没有任何作用，因为文件正在使用
	defer os.RemoveAll(nsPath)

	var wg sync.WaitGroup
	wg.Add(1)

	// 在专门的进程中进行命名空间相关的工作
	// 这样我们可以安全的Lock/Unlock OSThread而不会搞乱此函数调用者的lock/unlock状态
	go (func() {
		defer wg.Done()
		// 将当前协程绑到当前所在的OS线程上，确保它总是在此宿主机线程上执行
		// 在UnlockOSThread之前，其它协程不会在此OS线程上运行
		// 如果协程退出前没有UnlockOSThread则OS线程被关闭
		// 所有init函数在启动线程中运行，在其中调用LockOSThread会导致main函数在启动线程中执行
		// 在调用依赖于per-thread状态的OS服务或非Go库之前，应当LockOSThread
		runtime.LockOSThread()
		// 这里不去解锁，确保协程结束时OS线程会被杀死（1.10+）

		var origNS ns.NetNS
		// 获取当前线程的网络命名空间路径 /proc/2816046/task/2816057/ns/net
		origNS, err = ns.GetNS(getCurrentThreadNetNSPath())
		if err != nil {
			return
		}
		defer origNS.Close()

		// 在当前线程上创建新的网络命名空间
		err = unix.Unshare(unix.CLONE_NEWNET)
		if err != nil {
			return
		}

		// 恢复原来的网络命名空间
		defer origNS.Set()

		// 将当前线程的网络命名空间（/proc/..）绑定挂载到先前创建的挂载点上
		// 这会持久化网络命名空间，即使其中没有线程了（当前Goroutine的底层线程马上就退出了）
		err = unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "")
		if err != nil {
			err = fmt.Errorf("failed to bind mount ns at %s: %v", nsPath, err)
		}
	})()
	wg.Wait()

	if err != nil {
		return nil, fmt.Errorf("failed to create namespace: %v", err)
	}
	// 打开代表网络命名空间的文件描述符
	return ns.GetNS(nsPath)
}</pre>
<p>当不需要后，关闭文件描述符、解除挂载即可：</p>
<pre class="crayon-plain-tag">networkNS.Close()
testutils.UnmountNS(networkNS)

func UnmountNS(ns ns.NetNS) error {
	nsPath := ns.Path()
	// 仅当它是被bind挂载时才umount，不去触碰/proc中的命名空间
	if strings.HasPrefix(nsPath, getNsRunDir()) {
		if err := unix.Unmount(nsPath, 0); err != nil {
			return fmt.Errorf("failed to unmount NS: at %s: %v", nsPath, err)
		}

		if err := os.Remove(nsPath); err != nil {
			return fmt.Errorf("failed to remove ns path %s: %v", nsPath, err)
		}
	}

	return nil
} </pre>
<div class="blog_h1"><span class="graybg">参考</span></div>
<ol>
<li><a href="/cgroup-illustrated">控制组详解</a></li>
<li><a href="https://alexei-led.github.io/post/k8s_node_shell/">Get a Shell to a Kubernetes Node</a></li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/exec-host-cmd-from-within-a-pod">如何在Pod中执行宿主机上的命令</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/exec-host-cmd-from-within-a-pod/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>通过ExternalDNS集成外部DNS服务</title>
		<link>https://blog.gmem.cc/integrate-with-external-dns-provider</link>
		<comments>https://blog.gmem.cc/integrate-with-external-dns-provider#comments</comments>
		<pubDate>Sat, 29 Feb 2020 07:16:55 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=32651</guid>
		<description><![CDATA[<p>简介 ExternalDNS项目的目的是，将Kubernetes的Service/Ingress暴露的服务（的DNS记录）同步给外部的DNS Provider。 ExternalDNS的设计思想类似于KubeDNS，都是从多种K8S API资源中推断需要生成的DNS记录。不同之处是，ExternalDNS本身不提供DNS服务，它必须集成一个外部的DNS服务器，将DNS记录写进去。 大量场景下，使用ExternalDNS你可以基于K8S资源（主要是Ingress和LoadBalancer类型的Service）来动态的控制DNS记录，而不需要知晓DNS服务器的技术细节。这是因为ExternalDNS项目已经集成了多种知名DNS服务提供商。 快速起步 本章我们尝试以CodeDNS（启用Etcd插件）作为DNS Provider，安装和配置ExternalDNS，并将K8S中的Ingress、Service的DNS记录写到此CoreDNS中。 安装Etcd 在K8S中安装 [crayon-69e09a47b2aed594514126/] 运行为Docker容器 [crayon-69e09a47b2af1462130006/] 安装CoreDNS CoreDNS的Etcd插件，实现了SkyDNS风格的服务发现服务。该插件仅仅支持一部分DNS记录类型，因此不适合作为通用的DNS Zone数据插件。此外此插件不去处理subdomain、delegation。存储在Etcd中的数据，以SkyDNS消息的格式编码。Etcd插件通过forward插件的扩展用法，来将请求转发给网络上的服务器。 在K8S中安装 要启用Etcd插件，你需要修改stable/coredns的Values： [crayon-69e09a47b2af4550725937/] 然后进行安装：  [crayon-69e09a47b2af6864127723/] <a class="read-more" href="https://blog.gmem.cc/integrate-with-external-dns-provider">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/integrate-with-external-dns-provider">通过ExternalDNS集成外部DNS服务</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p><a href="https://github.com/kubernetes-sigs/external-dns">ExternalDNS</a>项目的目的是，将Kubernetes的Service/Ingress暴露的服务（的DNS记录）同步给外部的DNS Provider。</p>
<p>ExternalDNS的设计思想类似于KubeDNS，都是从多种K8S API资源中推断需要生成的DNS记录。不同之处是，ExternalDNS本身不提供DNS服务，它必须集成一个外部的DNS服务器，将DNS记录写进去。</p>
<p>大量场景下，使用ExternalDNS你可以基于K8S资源（主要是Ingress和LoadBalancer类型的Service）来动态的控制DNS记录，而不需要知晓DNS服务器的技术细节。这是因为ExternalDNS项目已经集成了多种知名DNS服务提供商。</p>
<div class="blog_h1"><span class="graybg">快速起步</span></div>
<p>本章我们尝试以CodeDNS（启用Etcd插件）作为DNS Provider，安装和配置ExternalDNS，并将K8S中的Ingress、Service的DNS记录写到此CoreDNS中。</p>
<div class="blog_h2"><span class="graybg">安装Etcd</span></div>
<div class="blog_h3"><span class="graybg">在K8S中安装</span></div>
<pre class="crayon-plain-tag">helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install bitnami/etcd --name kubefed-etcd --namespace kube-federation-system \
  --set global.imageRegistry=docker.gmem.cc \
  --set global.imagePullSecrets[0]=gmemregsecret  \
  --set auth.rbac.enabled=false  --set clusterDomain=k8s.gmem.cc</pre>
<div class="blog_h3"><span class="graybg">运行为Docker容器</span></div>
<pre class="crayon-plain-tag">docker run --name etcd -h etcd --network local --ip 172.21.0.14 --dns 172.21.0.1 \
  -e ALLOW_NONE_AUTHENTICATION=yes \
  docker.gmem.cc/bitnami/etcd:3.4.9</pre>
<div class="blog_h2"><span class="graybg">安装CoreDNS</span></div>
<p>CoreDNS的Etcd插件，实现了SkyDNS风格的服务发现服务。该插件仅仅支持一部分DNS记录类型，因此不适合作为通用的DNS Zone数据插件。此外此插件不去处理subdomain、delegation。存储在Etcd中的数据，以SkyDNS消息的格式编码。Etcd插件通过forward插件的扩展用法，来将请求转发给网络上的服务器。</p>
<div class="blog_h3"><span class="graybg">在K8S中安装</span></div>
<p>要启用Etcd插件，你需要修改stable/coredns的Values：</p>
<pre class="crayon-plain-tag">servers:
 - zones:
   - name: proxy
     parameters: . /etc/resolv.conf
   - name: etcd
     # 此插件是权威的 DNS Zones
     parameters: k8s.gmem.cc
     configBlock: |-
       # 在Etcd中的存储路径
       path /skydns
       # Etcd访问地址
       endpoint http://172.21.0.14:2379</pre>
<p>然后进行安装： </p>
<pre class="crayon-plain-tag">helm install coredns-1.10.1.tgz --name kubefed-coredns --namespace kube-federation-system \
  --set image.repository=docker.gmem.cc/coredns/coredns \
  --set isClusterService=false </pre>
<div class="blog_h3"><span class="graybg">运行为Docker容器</span></div>
<pre class="crayon-plain-tag">etcd k8s.gmem.cc {
  path /skydns
  endpoint http://172.21.0.14:2379
}
forward . 114.114.114.114</pre>
<div class="blog_h2"><span class="graybg">安装ExternalDNS</span></div>
<p>在这里我们安装一个ExternalDNS，并且将CoreDNS作为它的Provider：</p>
<pre class="crayon-plain-tag">helm install bitnami/external-dns --name external-dns --namespace=kube-system \
  --set global.imageRegistry=docker.gmem.cc \
  --set global.imagePullSecrets[0]=gmemregsecret  \
  --set provider=coredns \
  --set coredns.etcdEndpoints=http://172.21.0.14:2379 \
  --set sources="{service,ingress,istio-gateway,crd}" \
  --set publishInternalServices=true \
  --set crd.create=true --set policy=sync --set logLevel=debug</pre>
<div class="blog_h2"><span class="graybg">安装Ingress控制器</span></div>
<p>为了支持将Ingress Controller的外部IP地址设置给Ingress对象的Status中的IP地址，我们需要较新版本的<a href="https://github.com/helm/charts/tree/master/stable/nginx-ingress">nginx-ingress</a>：</p>
<pre class="crayon-plain-tag">helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx/ingress-nginx --name ingress-nginx --namespace kube-system
  --set controller.image.repository=docker.gmem.cc/kubernetes-ingress-controller/nginx-ingress-controller \
  # 将Ingress Controller的Service的IP地址报告为Ingress的Status的IP地址
  --set controller.publishService.enabled=true \
  # 使用的默认SSL证书
  --set controller.extraArgs."default-ssl-certificate"="kube-system/gmemk8scert" \
  # 配合MetalLB使用，让Ingress Controller的Service获得外部IP
  --set controller.service.type=LoadBalancer \
  --set controller.admissionWebhooks.patch.image.repository=docker.gmem.cc/jettech/kube-webhook-certgen \
  --set imagePullSecrets[0].name=gmemregsecret</pre>
<div class="blog_h2"><span class="graybg">测试</span></div>
<p>在K8S集群中创建一个Ingress，并等待其ADDRESS（status.loadBalancer.ingress.ip[0]）被填充：</p>
<pre class="crayon-plain-tag">kubectl -n devops get ingress  grafana 
# NAME      HOSTS                 ADDRESS      PORTS     AGE
# grafana   grafana.k8s.gmem.cc   10.0.11.10   80, 443   534d</pre>
<p>这时，ExternalDNS的日志中会出现DNS记录同步的相关信息： </p>
<p style="padding-left: 30px;">time="2020-06-01T02:46:34Z" level=debug msg="Endpoints generated from ingress: devops/grafana: [grafana.k8s.gmem.cc 0 IN A 10.0.11.10 [] grafana.k8s.gmem.cc 0 IN A 10.0.11.10 []]"</p>
<p>CoreDNS的后端Etcd中则会出现条目：</p>
<pre class="crayon-plain-tag">etcdctl --endpoints http://172.21.0.14:2379 get /skydns/cc/gmem/k8s/grafana --prefix
# /skydns/cc/gmem/k8s/grafana/0e5a5d35
# {"host":"10.0.11.10","text":"\"heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/devops/grafana\"","targetstrip":1}</pre>
<p>使用nslookup，针对CoreDNS进行DNS查询测试，可以发现解析能够成功。 </p>
<div class="blog_h1"><span class="graybg">命令行</span></div>
<div class="blog_h2"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 33%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--master=""</td>
<td>K8S API Server地址</td>
</tr>
<tr>
<td>--kubeconfig=""</td>
<td>使用的Kubeconfig</td>
</tr>
<tr>
<td>--request-timeout=30s</td>
<td>K8S API Server访问超时</td>
</tr>
<tr>
<td>--source=source ..</td>
<td>从哪些K8S资源中查找端点：service, ingress, node, fake, connector, istio-gateway, cloudfoundry,  contour-ingressroute, crd, empty, skipper-routegroup</td>
</tr>
<tr>
<td>--namespace=""</td>
<td>从哪些命名空间查找K8S资源，默认所有命名空间</td>
</tr>
<tr>
<td>--annotation-filter=""</td>
<td>基于注解过滤被external-dns管理的source列表，默认所有source</td>
</tr>
<tr>
<td>--fqdn-template=""</td>
<td>
<p>对于不提供hostname的源，使用该参数提供的模板生成DNS名。可以指定逗号分隔的列表 </p>
<p>和fake source联用时则指定hostname后缀</p>
</td>
</tr>
<tr>
<td>--combine-fqdn-annotation</td>
<td>combine fqdn-template和注解，而非覆盖</td>
</tr>
<tr>
<td>--ignore-hostname-annotation</td>
<td>提供了fqdn-template的情况下，生成DNS名称时忽略hostname注解</td>
</tr>
<tr>
<td>--publish-internal-services</td>
<td>发布ClusterIP类型Service的地址</td>
</tr>
<tr>
<td>--publish-host-ip</td>
<td>发布无头服务的host-ip</td>
</tr>
<tr>
<td>--always-publish-not-ready-addresses</td>
<td>总是发布无头服务的尚未就绪的地址</td>
</tr>
<tr>
<td>--crd-source-apiversion</td>
<td>CRD源的API版本，默认externaldns.k8s.io/v1alpha1</td>
</tr>
<tr>
<td>--crd-source-kind="DNSEndpoint"</td>
<td>CRD源的API类型</td>
</tr>
<tr>
<td>--service-type-filter</td>
<td>关注的Service类型，默认all，可选ClusterIP, NodePort, LoadBalancer, ExternalName</td>
</tr>
<tr>
<td>--provider=provider</td>
<td>使用的DNS Provider，可选aws, aws-sd, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns</td>
</tr>
<tr>
<td>--domain-filter= ...</td>
<td>限制处理的目标DNS Zone，使用domain后缀形式，该参数可以指定多次</td>
</tr>
<tr>
<td>--exclude-domains= ...</td>
<td>排除DNS子域</td>
</tr>
<tr>
<td>--zone-id-filter= ...</td>
<td>使用Zone ID来过滤目标Zone</td>
</tr>
<tr>
<td>--policy=sync</td>
<td>和DNS Provider进行数据同步的方式，默认sync，可选upsert-only, create-only</td>
</tr>
<tr>
<td>--registry=txt</td>
<td>跟踪DNS记录所有权的registry实现方式，默认txt，可选noop, aws-sd</td>
</tr>
<tr>
<td>--txt-owner-id="default"</td>
<td>使用txt registry时，用于识别当前ExternalDNS的ID</td>
</tr>
<tr>
<td>--txt-prefix=""</td>
<td>使用txt registry时，给每个所有权记录前缀的字符串</td>
</tr>
<tr>
<td>--txt-cache-interval=0s</td>
<td>txt缓存同步时间</td>
</tr>
<tr>
<td>--interval=1m0s</td>
<td>两次连续的，到DNS Provider的同步的间隔</td>
</tr>
<tr>
<td>--once</td>
<td>在第一次同步后，退出同步循环</td>
</tr>
<tr>
<td>--dry-run</td>
<td>打印DNS记录变更，不调用DNS Provider</td>
</tr>
<tr>
<td>--events</td>
<td>当source变更时间发生时，也（在定期同步的基础上）触发同步</td>
</tr>
<tr>
<td>--log-format=text</td>
<td>日志格式，可选text, json</td>
</tr>
<tr>
<td>--metrics-address=":7979"</td>
<td>指标和健康检查暴露地址</td>
</tr>
<tr>
<td>--log-level=info</td>
<td>日志级别 panic, debug, info, warning, error, fatal</td>
</tr>
</tbody>
</table>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/integrate-with-external-dns-provider">通过ExternalDNS集成外部DNS服务</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/integrate-with-external-dns-provider/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Kubefed学习笔记</title>
		<link>https://blog.gmem.cc/kubefed-study-note</link>
		<comments>https://blog.gmem.cc/kubefed-study-note#comments</comments>
		<pubDate>Wed, 26 Feb 2020 07:58:38 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=32597</guid>
		<description><![CDATA[<p>简介 联邦简介 集群联邦（Federation）的目的是实现单一集群统一管理多个Kubernetes集群的机制，这些集群可能是跨地区（Region），也可能是在不同公有云供应商上，亦或者是公司内部自行建立的集群。一但集群进行联邦后，就可以利用Federation API资源来统一管理多个集群的Kubernetes API资源，如定义Deployment如何部署到不同集群上，其集群所需的副本数等。 通过集群联邦，你可以： 简化管理多个集群的Kubernetes 组件，如Deployment, Service 等 在多个集群之间分散工作负载，以提升应用的可靠性 跨集群的资源编排，依据编排策略在多个集群进行应用部署 在不同集群中，能更快速更容易地迁移应用 跨集群的服务发现，服务可以实现地理位置感知，以降低延迟 实践多云（Multi-cloud）或混合云（Hybird Cloud）的部署 V1简介 Federation v1在Kubernetes v1.3左右时，就已经着手设计，并在Kubernetes v1.6时，进入了Beta阶段。之后一直没有发展，并在1.11被废弃。 V1在架构上有很多问题，包括： <a class="read-more" href="https://blog.gmem.cc/kubefed-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/kubefed-study-note">Kubefed学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<div class="blog_h2"><span class="graybg">联邦简介</span></div>
<p>集群联邦（Federation）的目的是实现单一集群统一管理多个Kubernetes集群的机制，这些集群可能是跨地区（Region），也可能是在不同公有云供应商上，亦或者是公司内部自行建立的集群。一但集群进行联邦后，就可以利用Federation API资源来统一管理多个集群的Kubernetes API资源，如定义Deployment如何部署到不同集群上，其集群所需的副本数等。</p>
<p>通过集群联邦，你可以：</p>
<ol>
<li><span style="background-color: #c0c0c0;">简化管理多个集群</span>的Kubernetes 组件，如Deployment, Service 等</li>
<li>在多个集群之间<span style="background-color: #c0c0c0;">分散工作负载</span>，以提升应用的可靠性</li>
<li><span style="background-color: #c0c0c0;">跨集群的资源编排</span>，依据编排策略在多个集群进行应用部署</li>
<li>在不同集群中，能更快速更容易地<span style="background-color: #c0c0c0;">迁移</span>应用</li>
<li><span style="background-color: #c0c0c0;">跨集群的服务发现</span>，服务可以实现<span style="background-color: #c0c0c0;">地理位置感知</span>，以降低延迟</li>
<li>实践多云（Multi-cloud）或混合云（Hybird Cloud）的部署</li>
</ol>
<div class="blog_h2"><span class="graybg">V1简介</span></div>
<p>Federation v1在Kubernetes v1.3左右时，就已经着手设计，并在Kubernetes v1.6时，进入了Beta阶段。之后一直没有发展，并在1.11被废弃。</p>
<p>V1在架构上有很多问题，包括：</p>
<ol>
<li>控制平面组件会因为发生问题，而影响整体集群效率</li>
<li>无法兼容新的Kubernetes API 资源</li>
<li>无法有效的在多个集群管理权限，如不支持RBAC</li>
<li>联邦层级的设定与策略依赖API 资源的Annotations</li>
</ol>
<p>Federation v1的控制平面类似于K8S，包括组件：</p>
<ol>
<li>federation-apiserver：提供Federation API资源，只支持部分Kubernetes API resources</li>
<li>federation-controller-manager：协调不同集群之间的状态，如同步Federated资源与策略</li>
<li>etcd：储存Federation的状态</li>
</ol>
<p>Federation v1的API Server通过k8s.io/apiserver套件开发，这种是采用<span style="background-color: #c0c0c0;">API Aggregation方式来扩充Kubernetes API</span>。</p>
<p>Federation v1写死了K8S资源版本，例如extensions/v1beta1的Deployment，写成apps/v1的Deployment则不支持。内置支持（管理的）K8S资源类型很少，如果想新增，必须在Federation types新增对应的Adapter，然后通过Code Generator产生API的client-go组件给Controller Manager操作API使用，最后重新建构一版本来更新API Server与Controller Manager以提供对其他资源与版本的支持，非常麻烦。</p>
<p>Federation v1在设计之初未考虑RBAC与CRD，这也是致命缺点。</p>
<div class="blog_h2"><span class="graybg">V2简介</span></div>
<p>Federation v2也叫Kubernetes Cluster Federation，或者KubeFed。和V1比起来，优势包括：</p>
<ol>
<li>简化扩展Federated API过程</li>
<li>加强跨集群服务发现与编排的功能</li>
<li>模块化、可定制化，可以随着K8S生态一起演进</li>
<li>移除了API Server，通过CRD机制来完成Federated Resources的扩充。这些资源由KubeFed Controller管理</li>
</ol>
<p>利用Kubefed（Kubernetes Cluster Federation）你可以用<span style="background-color: #c0c0c0;">宿主集群</span>（Host Cluster）中的<span style="background-color: #c0c0c0;">单套API</span>来协调<span style="background-color: #c0c0c0;">多个K8S集群的配置</span>。Kubefed刻意提供了低级别的API，用来应用复杂部署场景，包括跨地理位置应用部署、灾难恢复。</p>
<p>使用Kubefed你需要提供两类配置信息：</p>
<ol>
<li>Type配置：声明Kubefed需要处理的API类型</li>
<li>Cluster配置：声明Kubefed需要管理的目标集群</li>
</ol>
<p>术语 Propagation 表示将资源分发到对应集群的机制。</p>
<p>Type配置包含三个基本要素：</p>
<ol>
<li>Templates 定义资源在所有集群中共同的部分</li>
<li>Placement 定义资源应该出现在哪些集群中</li>
<li>Overrides 定义集群对Template字段的覆盖</li>
</ol>
<p>高层API基于这些基本要素：</p>
<ol>
<li>Status：收集被分发到所有集群的资源的状态</li>
<li>Policy：决定一个资源应当被分发到联邦集群的什么子集中</li>
<li>Scheduling：决定工作负载如何跨越不同集群分布</li>
</ol>
<div class="blog_h2"><span class="graybg">术语</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">Federate</td>
<td>将若干K8S集群联邦起来，提供一套公共接口，可以针对这个集群池操作，从而跨越多个K8S集群部署应用程序</td>
</tr>
<tr>
<td class="blog_h3">KubeFed</td>
<td>Kubefed是v2版本的K8S集群联邦，让用户联合多个K8S集群，实现：资源分发、服务发现、跨集群高可用</td>
</tr>
<tr>
<td class="blog_h3">Host Cluster</td>
<td>一个用于暴露KubeFed API、运行KubeFet控制平面的集群</td>
</tr>
<tr>
<td class="blog_h3">Cluster Registration</td>
<td>一个K8S集群可以通过<pre class="crayon-plain-tag">kubefedctl join</pre>加入到Host Cluster（加入到联邦）</td>
</tr>
<tr>
<td class="blog_h3">Member Cluster</td>
<td>
<p>一个集群通过KubeFed API注册并加入到联邦，则成为成员，KubeFed控制器在此之后获得该集群的访问凭证</p>
<p>Host Cluster是Member Cluster</p>
</td>
</tr>
<tr>
<td class="blog_h3">ServiceDNSRecord</td>
<td>一个关联到1-N个K8S Service的、包含了这些Service访问方式的资源。这种资源中还包括构造Service的DNS记录的Scheme</td>
</tr>
<tr>
<td class="blog_h3">IngressDNSRecord</td>
<td>一个关联到1-N个K8S Ingress的、包含了这些Ingress访问方式的资源。这种资源中还包括构造Ingress的DNS记录的Scheme</td>
</tr>
<tr>
<td class="blog_h3">DNSEndpoint</td>
<td>包装Endpoint资源的CRD</td>
</tr>
<tr>
<td class="blog_h3">Endpoint</td>
<td>表示DNS资源记录的资源</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">安装和配置</span></div>
<div class="blog_h2"><span class="graybg">安装</span></div>
<div class="blog_h3"><span class="graybg">kubefedctl</span></div>
<p>直接到：https://github.com/kubernetes-sigs/kubefed/releases 下载即可。</p>
<div class="blog_h3"><span class="graybg">控制平面</span></div>
<p>KubeFed可以在kind、Minikube、GKE等多种K8S环境下运行。你可以通过Chart方式来安装：</p>
<pre class="crayon-plain-tag">helm repo add kubefed  https://raw.githubusercontent.com/kubernetes-sigs/kubefed/master/charts

helm install  kubefed/kubefed --name kubefed  --namespace kube-federation-system \
  --set controllermanager.replicaCount=1 \
  --set controllermanager.repository=docker.gmem.cc/kubernetes-multicluster</pre>
<div class="blog_h3"><span class="graybg">卸载</span></div>
<p>卸载时可能导致命名空间一直处于Terminating状态。根因是FederatedTypeConfig类型的CR上具有finalizer core.kubefed.io/federated-type-config。此finalizer导致CRD卡死在InstanceDeletionInProgress状态。</p>
<p>相关Issue：<a href="https://github.com/kubernetes/kubernetes/issues/60538">https://github.com/kubernetes/kubernetes/issues/60538</a></p>
<div class="blog_h2"><span class="graybg">联邦成员管理</span></div>
<div class="blog_h3"><span class="graybg">加入联邦</span></div>
<p>可以通过kubefedctl来将集群加入到联邦，你需要将所有集群的配置信息收集到KUBECONFIG中，分别作为一个context。</p>
<pre class="crayon-plain-tag"># 你需要修改默认的context名称，因为其中的@是特殊字符，会导致报错
kubefedctl join k8s  --cluster-context k8s  --host-cluster-context k8s --v=2
kubefedctl join kind --cluster-context kind --host-cluster-context k8s --v=2</pre>
<div class="blog_h3"><span class="graybg">查看成员列表</span></div>
<pre class="crayon-plain-tag">kubectl -n kube-federation-system get kubefedclusters
# NAME   AGE    READY
# k8s    117m   True
# 如果READY不为True，则提示联邦控制平面无法访问到该成员的API Server
# kind   117m  </pre>
<div class="blog_h3"><span class="graybg">踢出联邦</span></div>
<pre class="crayon-plain-tag">kubefedctl unjoin kind --cluster-context=kind --host-cluster-context=k8s</pre>
<div class="blog_h2"><span class="graybg">联邦API配置</span></div>
<p>你可以启用任何K8S API类型的联邦支持，包括CRD。只有启用了联邦支持的API，才能够由联邦控制平面管理，分发给成员集群。</p>
<div class="blog_h3"><span class="graybg">启用内置API联邦</span></div>
<p>ClusterRole、Configmap、Deployment、Ingress、Job、Namespace、ReplicaSet、Secrets、ServiceAccount、Service默认情况下已经启用。</p>
<div class="blog_h3"><span class="graybg">指定API组</span></div>
<p>假设你有一个API名为deployment.k8s.gmem.cc，由于它和内置的Deployment资源同名，在启用它的联邦时，你需要指定一个组名：</p>
<pre class="crayon-plain-tag">kubefedctl enable deployments.gmem.cc --federated-group kubefed.gmem.cc</pre>
<p>原因是deployment.types.kubefed.io这个名字已经被占用。</p>
<div class="blog_h3"><span class="graybg">启用CRD联邦</span></div>
<p>要支持你的自定义资源（CR）的联邦，必须启用customresourcedefinitions（CRD）的联邦：</p>
<pre class="crayon-plain-tag">kubefedctl enable customresourcedefinitions</pre>
<p>上述命令执行后，会创建一个CRD：federatedcustomresourcedefinitions.types.kubefed.io。并且，在在Kubefed的控制平面所在命名空间，会创建一个federatedtypeconfigs.core.kubefed.io：</p>
<pre class="crayon-plain-tag">kubectl -n kube-federation-system get federatedtypeconfigs.core.kubefed.io
# federatedcustomresourcedefinitions.types.kubefed.io   2020-05-27T11:10:59Z</pre>
<div class="blog_h3"><span class="graybg">执行CRD实例的联邦</span></div>
<p>执行下面的命令执行某种CRD的联邦：</p>
<pre class="crayon-plain-tag">kubefedctl federate crd envoyfilters.networking.istio.io</pre>
<p>上述命令会执行类型为CRD的，名字为envoyfilters.networking.istio.io的资源的联邦。它会创建一个federatedcustomresourcedefinitions.types.kubefed.io资源，在其中描述如何在联邦中分发这个CRD：</p>
<pre class="crayon-plain-tag">apiVersion: types.kubefed.io/v1beta1
kind: FederatedCustomResourceDefinition
metadata:
  finalizers:
  - kubefed.io/sync-controller
  name: envoyfilters.networking.istio.io
spec:
  # 需要分发到哪些集群
  placement:
    clusterSelector:
      matchLabels: {}
  # 模板，即EnvoyFilter这个CRD的内容
  template:
    metadata:
      labels:
        app: istio-pilot
        chart: istio
    spec:
      conversion:
        strategy: None
      group: networking.istio.io
      names:
        categories:
        - istio-io
        - networking-istio-io
        kind: EnvoyFilter
status:
  # 已经分发的集群列表
  clusters:
  - name: kind
  - name: k8s</pre>
<p>如果一切顺利，你应该很快在kind、k8s这两个集群中，看到名为envoyfilters.networking.istio.io的CRD。</p>
<div class="blog_h3"><span class="graybg">启用CR联邦</span></div>
<p>执行了CRD的联邦了，CRD就可以分发到成员集群了。在此前提下，你可以启用对应CR的联邦：</p>
<pre class="crayon-plain-tag">kubefedctl enable envoyfilters.networking.istio.io</pre>
<p>上述命令会：</p>
<ol>
<li>创建一个名为federatedenvoyfilters.types.kubefed.io的CRD</li>
<li>在Kubefed控制平面所在命名空间，创建一个名为envoyfilters.networking.istio.io的FederatedTypeConfig</li>
</ol>
<p>FederatedTypeConfig负责将联邦类型和目标类型关联启用，启用联邦资源（转换为目标类型）传播（到成员集群）：</p>
<pre class="crayon-plain-tag">apiVersion: core.kubefed.io/v1beta1
kind: FederatedTypeConfig
metadata:
  finalizers:
  - core.kubefed.io/federated-type-config
  name: envoyfilters.networking.istio.io
  namespace: kube-federation-system
spec:
  # 联邦类型
  federatedType:
    group: types.kubefed.io
    kind: FederatedEnvoyFilter
    pluralName: federatedenvoyfilters
    scope: Namespaced
    version: v1beta1
  # 是否启用分发
  propagation: Enabled
  # 目标类型
  targetType:
    group: networking.istio.io
    kind: EnvoyFilter
    pluralName: envoyfilters
    scope: Namespaced
    version: v1alpha3
status:
  observedGeneration: 1
  propagationController: Running
  statusController: NotRunning</pre>
<div class="blog_h3"><span class="graybg">执行CR的联邦</span></div>
<p>到这里，你就可以执行某个EnvoyFilter的联邦了 —— 指定哪些资源，需要分发到哪些集群中，各集群如何覆盖资源的某些字段。</p>
<p>这是最关键的主题，我们在下一章详细学习。</p>
<div class="blog_h3"><span class="graybg">禁用传播</span></div>
<p>要禁止某一类API资源的分发，需要将FederatedTypeConfig的propagation设置为false：</p>
<pre class="crayon-plain-tag">kubectl patch --namespace kube-federation-system federatedtypeconfigs envoyfilters.networking.istio.io \
    --type=merge -p '{"spec": {"propagation": "Disabled"}}'</pre>
<p>这样SyncController就会停止 envoyfilters的分发。</p>
<p>如果要永久的禁用联邦，则可以执行disable命令：</p>
<pre class="crayon-plain-tag">kubefedctl disable envoyfilters.networking.istio.io</pre>
<p>对应的FederatedTypeConfig资源会被删除。 </p>
<div class="blog_h2"><span class="graybg">MCIDNS配置</span></div>
<p>这里我们以CoreDNS（启用Etcd插件）作为<a href="/integrate-with-external-dns-provider">ExternalDNS</a>的DNS Provider，利用<a href="/external-lb-for-on-premise-k8s-cluster">MetalLB</a>来提供LoadBalancer类型的Service，来试验MCIDNS功能。</p>
<p>CoreDNS、Etcd、ExternalDNS、MetalLB的安装步骤这里不详述，可以参考上段文字中的连接。本文强调一下用在Kubefed场景下差异化的部分。</p>
<p>ExternalDNS安装时，使用如下配置：</p>
<pre class="crayon-plain-tag">helm install bitnami/external-dns --name kubefed-external-dns --namespace=kube-federation-system \
  --set global.imageRegistry=docker.gmem.cc \
  --set global.imagePullSecrets[0]=gmemregsecret  \
  --set provider=coredns \
  --set coredns.etcdEndpoints=http://kubefed-etcd:2379 \
  --set sources={crd} \
  --set crd.apiversion=multiclusterdns.kubefed.io/v1alpha1 \
  --set crd.kind=DNSEndpoint \
  --set logLevel=debug</pre>
<div class="blog_h1"><span class="graybg">执行联邦</span></div>
<p>执行某个资源的联邦就是：<span style="background-color: #c0c0c0;">修改某个的资源模板，为某些成员集群应用差异化的模板变量，然后分发到这些成员集群中</span>。其前提条件是启用该资源类型的联邦支持，在上一章我们已经讨论如何做到这一点。</p>
<div class="blog_h2"><span class="graybg">命令</span></div>
<p><pre class="crayon-plain-tag">kubefedctl federate</pre>命令负责执行联邦，它会以一个现有的目标资源为模板（Template），创建对应的联邦资源，默认情况下以所有成员集群为分发（Placement）目标。命令格式如下：</p>
<pre class="crayon-plain-tag">kubefedctl federate &lt;target kubernetes API type&gt; &lt;target resource&gt; [flags]

# 可用标记
# -c, --contents      仅当联邦命名空间时有意义，如果指定，则在联邦完命名空间后，
#                     联邦其中的资源
# -s, --skip-api-resources
#                     -c时，需要跳过的资源类型
# --dry-run           不发起API请求
# -f, --filename      从文件中读取需要联邦的资源列表，并且仅仅是将联邦后的结果输出到标准输出
# -n, --namespa       目标资源所在命名空间
# -o, --output        输出到标准输出，而非创建联邦资源</pre>
<div class="blog_h2"><span class="graybg">联邦一个资源</span></div>
<p>下面的例子，联邦了位于my-namespace中的ConfigMap my-configmap：</p>
<pre class="crayon-plain-tag">kubefedctl federate configmaps my-configmap -n my-namespace</pre>
<div class="blog_h2"><span class="graybg">联邦命名空间</span></div>
<p>联邦命名空间时，你可以选择将其中的所有/部分类型资源都进行联邦：</p>
<pre class="crayon-plain-tag">kubefedctl federate namespace my-namespace --contents --skip-api-resources "configmaps,apps"</pre>
<div class="blog_h2"><span class="graybg">查看传播状态 </span></div>
<p>要了解联邦资源分发的进度，你需要查看联邦资源的状态：</p>
<pre class="crayon-plain-tag">apiVersion: types.kubefed.io/v1beta1
kind: FederatedNamespace
metadata:
  name: myns
  namespace: myns
spec:
  placement:
    clusterSelector: {}
status:
  conditions:
  - type: Propagation
    # True意味着分发已经完毕
    status: True
    lastTransitionTime: "2019-05-08T01:23:20Z"
    lastUpdateTime: "2019-05-08T01:23:20Z"
  # 下面两个集群已经接收到最新的myns
  clusters:
  - name: cluster1
  - name: cluster2</pre>
<p>如果Propagation的Status为False，则Reason会说明分发未完成的原因：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">原因</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>CheckClusters</td>
<td>
<p>一个或多个集群不在期望状态。这种情况下，clusters.status字段会显示具体原因：</p>
<pre class="crayon-plain-tag">apiVersion: types.kubefed.io/v1beta1
kind: FederatedNamespace
metadata:
  name: myns
  namespace: myns
spec:
  placement:
    clusters:
    - name: cluster1
status:
  conditions:
  - type: Propagation
    status: False
    reason: CheckClusters
    lastTransitionTime: "2019-05-08T01:23:20Z"
    lastUpdateTime: "2019-05-08T01:23:20Z"
  clusters:
  - name: cluster1
  - name: cluster2
    status: DeletionFailed </pre>
</td>
</tr>
<tr>
<td>ClusterRetrievalFailed</td>
<td>无法获取成员集群</td>
</tr>
<tr>
<td>ComputePlacementFailed</td>
<td>计算Placement时出错</td>
</tr>
<tr>
<td>NamespaceNotFederated</td>
<td>包含资源的命名空间没有被联邦</td>
</tr>
</tbody>
</table>
<p>除了CheckClusters，其它原因都会产生一个K8S事件。
<p>CheckClusters时，可能出现的某个集群错误status包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">Status</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>AlreadyExists</td>
<td>目标资源已经存在于集群，但是由于adoptResources被禁用，无法收养（adopted）</td>
</tr>
<tr>
<td>ApplyOverridesFailed</td>
<td>向模板覆盖字段时出错</td>
</tr>
<tr>
<td>CachedRetrievalFailed</td>
<td>提取缓存的目标资源时出错</td>
</tr>
<tr>
<td>ClientRetrievalFailed</td>
<td>创建目标成员集群的API客户端时出错</td>
</tr>
<tr>
<td>ClusterNotReady</td>
<td>目标集群的健康检查失败</td>
</tr>
<tr>
<td>ComputeResourceFailed</td>
<td>生成资源在目标集群中应有形式时失败</td>
</tr>
<tr>
<td>CreationFailed</td>
<td>创建目标资源失败</td>
</tr>
<tr>
<td>CreationTimedOut</td>
<td>创建目标资源超时</td>
</tr>
<tr>
<td>DeletionFailed</td>
<td>删除失败</td>
</tr>
<tr>
<td>DeletionTimedOut</td>
<td>删除超时</td>
</tr>
<tr>
<td>FieldRetentionFailed</td>
<td>Retain一个或多个字段时出错，例如Service的clusterIP</td>
</tr>
<tr>
<td>LabelRemovalFailed</td>
<td>移除KubeFed标签失败</td>
</tr>
<tr>
<td>LabelRemovalTimedOut</td>
<td>移除KubeFed标签超时</td>
</tr>
<tr>
<td>ManagedLabelFalse</td>
<td>无法管理具有kubefed.io/managed: false标签的资源</td>
</tr>
<tr>
<td>RetrievalFailed</td>
<td>无法从目标集群检索资源</td>
</tr>
<tr>
<td>UpdateFailed</td>
<td>更新失败</td>
</tr>
<tr>
<td>UpdateTimedOut</td>
<td>更新超时</td>
</tr>
<tr>
<td>VersionRetrievalFailed</td>
<td>无法获取最新版本的资源</td>
</tr>
<tr>
<td>WaitingForRemoval</td>
<td>目标资源已经被标记为正在删除，等待垃圾回收器</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">删除策略</span></div>
<p>由Sync Controller管理的联邦资源，具有Finalizer：kubefed.io/sync-controller。此Finalizer阻止资源的删除，直到Sync Controller有机会执行前置清理操作。</p>
<p>前置清理操作，就是要从受管成员集群中移除联邦资源对应的目标资源。</p>
<p>如果想在删除联邦资源后，仍然保留目标资源，则需要在执行删除操作之前，给联邦资源加上注解：<pre class="crayon-plain-tag">kubefed.io/orphan: true</pre>，你可以通过命令完成：</p>
<pre class="crayon-plain-tag"># 启用孤儿化
kubefedctl orphaning-deletion enable &lt;federated type&gt; &lt;name&gt;
# 查看孤儿化状态
kubefedctl orphaning-deletion status &lt;federated type&gt; &lt;name&gt;
# 禁用孤儿化
kubefedctl orphaning-deletion disable &lt;federated type&gt; &lt;name&gt;</pre>
<div class="blog_h2"><span class="graybg">Placement配置</span></div>
<p>要指定某个联邦资源需要分发到哪些集群，只需要设置联邦资源的spec.placement：</p>
<pre class="crayon-plain-tag">spec:
  placement:
    # 根据集群名字
    clusters:
    - name: cluster1
    # 根据集群标签
    # 这样打标签：kubectl label kubefedclusters -n kube-federation-system cluster1 foo=bar
    clusterSelector:
      foo: bar</pre>
<p>一旦设置，<span style="background-color: #c0c0c0;">已经分发到cluster2的资源，会自动删除</span>。 </p>
<p>如果placement设置为<pre class="crayon-plain-tag">{}</pre>则不分发到任何集群。</p>
<div class="blog_h2"><span class="graybg">Overrides配置</span></div>
<p>在分发资源给目标集群时，你可以进行差异化的修改。修改在联邦资源的spec.overrides中进行，语法是jsonpatch的子集：</p>
<ol>
<li>op 定义执行的操作：
<ol>
<li>replace 替换，默认</li>
<li>add 添加到对象或数组</li>
<li>remove 从对象或数组移除</li>
</ol>
</li>
<li>path 定义需要操作的字段所在路径，必须以/开始：
<ol>
<li>例如 /spec/replicas</li>
<li>索引语法  /spec/template/spec/containers/0/image</li>
</ol>
</li>
<li>value 定义字段的值。对于remove操作，忽略</li>
</ol>
<p>下面是一个例子：</p>
<pre class="crayon-plain-tag">kind: FederatedDeployment
...
spec:
  ...
  overrides:
    # 针对cluster1的Override
    - clusterName: cluster1
      clusterOverrides:
        # 设置副本数为5
        - path: "/spec/replicas"
          value: 5
        # 设置第一个容器的镜像
        - path: "/spec/template/spec/containers/0/image"
          value: "nginx:1.17.0-alpine"
        # 确保注解存在
        - path: "/metadata/annotations"
          op: "add"
          value:
            foo: bar
        # 确保注解删除
        - path: "/metadata/annotations/foo"
          op: "remove"
        # 插入第一个容器命令参数
        - path: "/spec/template/spec/containers/0/args/0"
          op: "add"
          value: "-q"</pre>
<div class="blog_h3"><span class="graybg">覆盖保留字段</span></div>
<p>在计算目标集群中，联邦资源应该生成的形态时，使用如下算法：</p>
<ol>
<li>从Template 计算出新的资源</li>
<li>如果该资源已经存在于目标集群，应当保留（retention）的字段，被保留</li>
<li>执行override</li>
<li>设置受管labels</li>
</ol>
<div class="blog_h2"><span class="graybg">本地值保留</span></div>
<p>目标集群中资源的大部分字段，都会被从联邦资源生成的最新版本代替，例外如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 120px; text-align: center;">资源类型</td>
<td style="width: 200px; text-align: center;">字段</td>
<td style="width: 100px; text-align: center;">保留策略</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>All</td>
<td>metadata.annotations</td>
<td>Always</td>
<td>期望被成员集群中某些控制器管理的注解</td>
</tr>
<tr>
<td>All</td>
<td>metadata.finalizers</td>
<td>Always</td>
<td>期望被成员集群中某些控制器管理的Finazlier</td>
</tr>
<tr>
<td>All</td>
<td>metadata.resourceVersion</td>
<td>Always</td>
<td>用于并发控制</td>
</tr>
<tr>
<td>Scalable</td>
<td>spec.replicas</td>
<td>Conditional</td>
<td>HPA控制器可能负责管理该字段</td>
</tr>
<tr>
<td>Service</td>
<td>spec.clusterIP<br />spec.ports</td>
<td>Always</td>
<td>某个控制器可能负责修改这些字段 </td>
</tr>
<tr>
<td>ServiceAccount</td>
<td>secrets</td>
<td>Conditional</td>
<td>某个控制器可能负责修改这些字段 </td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">高级特性</span></div>
<p>利用Kubefed提供的底层FederatedXxx API（具有template、placement、override字段），以及关联的控制器，我们可以构建出高级别的API。Kubefed自身提供了一些高级别API。</p>
<div class="blog_h2"><span class="graybg">MCIDNS</span></div>
<p>多集群Ingress DNS（Multi-Cluster Ingress DNS）提供以编程方式管理K8S Ingress资源的DNS记录的能力。MCIDNS的目的不是替换CoreDNS这样的DNS Provider，而是通过集成ExternalDNS，来在支持DNS Provider（例如一些云服务商的DNS服务）中管理外部DNS记录。</p>
<div class="blog_h3"><span class="graybg">工作流程</span></div>
<p>MCIDNS的工作流程图：</p>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/05/ingressdns-with-externaldns.png"><img class="wp-image-32649 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/05/ingressdns-with-externaldns.png" alt="ingressdns-with-externaldns" width="680" height="510" /></a></p>
<p>&nbsp;</p>
<p>典型的工作流说明：</p>
<ol>
<li>用户创建FederatedDeployment、FederatedService、FederatedIngress资源</li>
<li>Kubefed的Sync Controller将对应的目标资源传播到成员K8S集群中</li>
<li>用户创建一个IngressDNSRecord资源，此资源标识期望的DNS名，以及可选的DNS记录参数</li>
<li>MCIDNS的Ingress DNS Controller监控IngressDNSRecord，用匹配（成员集群中命名空间、名字与IngressDNSRecord一致的）的Ingress资源的IP地址（这个地址由Ingress Controller填充）来更新IngressDNSRecord的Status</li>
<li>MCIDNS的DNS Endpoint Controller监控IngressDNSRecord，创建对应的DNSEndpoint</li>
<li>DNSEndpoint包含必要的信息，外部DNS系统（例如ExternalDNS）负责监控此对象，利用其中的信息，在DNS Provider中创建DNS记录</li>
</ol>
<div class="blog_h3"><span class="graybg">如何启用</span></div>
<p>要启用MCIDNS，你首先需要创建Kubefed控制平面，并加入成员集群。然后：</p>
<ol>
<li>如有必要，在DNS Provider创建一个域名，或者，将某个DNS子域代理给ExternalDNS。具体操作方式咨询你的DNS Provider</li>
<li>安装ExternalDNS，必须指定参数：<br />
<pre class="crayon-plain-tag">--source=crd 
--crd-source-apiversion=multiclusterdns.kubefed.io/v1alpha1 
--crd-source-kind=DNSEndpoint 
--registry=txt 
--txt-prefix=cname</pre>
</li>
<li>
<p>创建一个样例的联邦 Deployment+Service+Ingress来进行测试：</p>
<ol>
<li>Ingress的域名设置为第一步中提及的域名的子域名</li>
<li>等待Ingress Controller将Ingress的IP地址设置好</li>
<li>创建IngressDNSRecord资源，必须注意，hosts字段和FederatedIngress的host字段要匹配：<br />
<pre class="crayon-plain-tag">apiVersion: multiclusterdns.kubefed.io/v1alpha1
kind: IngressDNSRecord
metadata:
  name: test-ingress
  namespace: test-namespace
spec:
  hosts:
  - test.kubefed.gmem.cc
  recordTTL: 300</pre>
</li>
<li>
<p>DNS Endpoint Controller将会使用目标Ingress中的IP地址来生成DNSEndpoint的targets字段，例如：</p>
<pre class="crayon-plain-tag">apiVersion: multiclusterdns.kubefed.io/v1alpha1
kind: DNSEndpoint
metadata:
  name: ingress-test-ingress
  namespace: test-namespace
spec:
  endpoints:
  - dnsName: ingress.example.com
    recordTTL: 300
    recordType: A
    targets:
    - $CLUSTER1_INGRESS_IP
    - $CLUSTER2_INGRESS_IP
status: {}</pre>
</li>
<li>ExternalDNS的控制器会监控DNSEndpoint对象，在DNS Provider中为每个目标Ingress创建A记录和TXT记录。例如：<br />
<pre class="crayon-plain-tag">NAME                   TYPE  TTL    DATA
test.kubefed.gmem.cc.  A     300    $CLUSTER1_INGRESS_IP,$CLUSTER2_INGRESS_IP
test.kubefed.gmem.cc.  TXT   300    "heritage=external-dns,external-dns/owner=my-identifier" </pre>
</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">MCSDNS</span></div>
<p>和MCIDNS类似，区别：
<ol>
<li>ServiceDNSRecord监控Service的IP地址，类似于IngressDNSRecord从Ingress同步IP地址信息</li>
<li>DNSEndpoint控制器会创建3个A记录，格式分别为：<br />
<pre class="crayon-plain-tag">&lt;service&gt;.&lt;namespace&gt;.&lt;federation&gt;.svc.&lt;federation-domain&gt; 
&lt;service&gt;.&lt;namespace&gt;.&lt;federation&gt;.svc.&lt;region&gt;.&lt;federation-domain&gt; 
&lt;service&gt;.&lt;namespace&gt;.&lt;federation&gt;.svc.&lt;availability-zone&gt;.&lt;region&gt;.&lt;federation-domain&gt;</pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">如何启用</span></div>
<p>参考如下步骤：</p>
<ol>
<li>准备Kubefed环境，需要支持LoadBalancer类型的服务</li>
<li>Ingress的域名设置为第一步中提及的域名的子域名</li>
<li>安装ExternalDNS</li>
<li>创建一个样例的联邦 Deployment+Service（LoadBalancer）来进行测试：
<ol>
<li>服务的EXTERNAL-IP就绪后，创建Domain和ServiceDNSRecord：<br />
<pre class="crayon-plain-tag">apiVersion: multiclusterdns.kubefed.io/v1alpha1
kind: Domain
metadata:
  # 对应DNS记录中的 &lt;federation&gt; 段
  name: test-domain
  # 运行 kubefed-controller-manager的命名空间
  namespace: kube-federation-system
# 在DNS Provider中配置好的域名或子域名
domain: kubefed.gmem.cc

---

apiVersion: multiclusterdns.kubefed.io/v1alpha1
kind: ServiceDNSRecord
metadata:
  # 测试服务的名字
  name: test-service
  # 测试服务的命名空间
  namespace: test-namespace
spec:
  # 引用Domain对象
  domainRef: test-domain
  recordTTL: 300</pre>
</li>
<li>
<p> DNSEndpoint会自动创建3个A记录、3个TXT记录：</p>
<pre class="crayon-plain-tag">test-service.test-namespace.test-domain.svc.kubefed.gmem.cc.                      A     300    $CLUSTER1_SERVICE_IP,$CLUSTER2_SERVICE_IP
test-service.test-namespace.test-domain.svc.kubefed.gmem.cc.                      TXT   300    "heritage=external-dns,external-dns/owner=my-identifier"
test-service.test-namespace.test-domain.svc.us-west1.kubefed.gmem.cc.             A     300    $CLUSTER1_SERVICE_IP,$CLUSTER2_SERVICE_IP
test-service.test-namespace.test-domain.svc.us-west1.kubefed.gmem.cc.             TXT   300    "heritage=external-dns,external-dns/owner=my-identifier"
test-service.test-namespace.test-domain.svc.us-west1-b.us-west1.kubefed.gmem.cc.  A     300    $CLUSTER1_SERVICE_IP,$CLUSTER2_SERVICE_IP
test-service.test-namespace.test-domain.svc.us-west1-b.us-west1.kubefed.gmem.cc.  TXT   300    "heritage=external-dns,external-dns/owner=my-identifier" </pre>
</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">ReplicaSchedulingPreference</span></div>
<p>RSP提供了一种机制，来自动化的机制来维护所有<span style="background-color: #c0c0c0;">成员集群中</span>、Deployment/Replicaset类型工作负载的<span style="background-color: #c0c0c0;">副本总数</span>。
<p>在用户提供的偏好（RSP的某些字段）中，指定了<span style="background-color: #c0c0c0;">集群的权重、副本数量最小、最大值</span>。如果某些集群中Pod<span style="background-color: #c0c0c0;">维持在unscheduled状态，还可以配置重新分发副本</span>（给其它集群）。</p>
<p>RSP控制器负责监控RSP资源，以及namespace/name匹配的FederatedDeployment或（取决于spec.targetKind）FederatedReplicaset资源。如果目标资源存在，它会根据配置，将 spec.totalReplicas分发到当前健康成员集群中。如果没有设置针对单个集群的偏好，则均匀分发。</p>
<p>如果RSP对象存在，则匹配的FederatedXxx资源中的副本数声明被忽略。RSP控制器负责更新FederatedXxx中的副本Overrides，实际传播工作仍然由Sync Controller完成。</p>
<p>如果指定了spec.rebalance，则RSP控制器会进行副本数再平衡，即将无法调度的Pod（成员集群资源不足）调度到其它健康集群中。</p>
<div class="blog_h3"><span class="graybg">均匀分布</span></div>
<pre class="crayon-plain-tag">apiVersion: scheduling.kubefed.io/v1alpha1
kind: ReplicaSchedulingPreference
metadata:
  name: test-deployment
  namespace: test-ns
spec:
  targetKind: FederatedDeployment
  totalReplicas: 9

# 或者

apiVersion: scheduling.kubefed.io/v1alpha1
kind: ReplicaSchedulingPreference
metadata:
  name: test-deployment
  namespace: test-ns
spec:
  targetKind: FederatedDeployment
  totalReplicas: 9
  clusters:
    "*":
      weight: 1</pre>
<div class="blog_h3"><span class="graybg">权重分布</span></div>
<pre class="crayon-plain-tag">apiVersion: scheduling.kubefed.io/v1alpha1
kind: ReplicaSchedulingPreference
metadata:
  name: test-deployment
  namespace: test-ns
spec:
  targetKind: FederatedDeployment
  totalReplicas: 9
  clusters:
    A:
      weight: 1
    B:
      weight: 2</pre>
<div class="blog_h3"><span class="graybg">限制副本数范围</span> </div>
<pre class="crayon-plain-tag">apiVersion: scheduling.kubefed.io/v1alpha1
kind: ReplicaSchedulingPreference
metadata:
  name: test-deployment
  namespace: test-ns
spec:
  targetKind: FederatedDeployment
  totalReplicas: 9
  clusters:
    A:
      minReplicas: 4
      maxReplicas: 6
      weight: 1
    B:
      minReplicas: 4
      maxReplicas: 8
      weight: 2</pre>
<div class="blog_h1"><span class="graybg">参考资料</span></div>
<ol>
<li>https://github.com/kubernetes-sigs/kubefed</li>
<li>https://www.kubernetes.org.cn/5702.html</li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/kubefed-study-note">Kubefed学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/kubefed-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>限制Pod磁盘空间用量</title>
		<link>https://blog.gmem.cc/limit-disk-usage-for-pods</link>
		<comments>https://blog.gmem.cc/limit-disk-usage-for-pods#comments</comments>
		<pubDate>Wed, 12 Feb 2020 02:56:44 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=32767</guid>
		<description><![CDATA[<p>Pod如何使用磁盘 容器在运行期间会产生临时文件、日志。如果没有任何配额机制，则某些容器可能很快将磁盘写满，影响宿主机内核和所有应用。 容器的临时存储，例如emptyDir，位于目录/var/lib/kubelet/pods下： [crayon-69e09a47b38c8752108828/] 持久卷的挂载点也位于/var/lib/kubelet/pods下，但是不会导致存储空间的消耗。 容器的日志，存放在/var/log/pods目录下。 使用Docker时，容器的rootfs位于/var/lib/docker下，具体位置取决于存储驱动。 Pod驱逐机制 磁盘容量不足触发的驱逐 具体细节参考：/kubernetes-study-note#out-of-resource。 当不可压缩资源（内存、磁盘）不足时，节点上的Kubelet会尝试驱逐掉某些Pod，以释放资源，防止整个系统受到影响。 其中，磁盘资源不足的信号来源有两个： imagefs：容器运行时用作存储镜像、可写层的文件系统 nodefs：Kubelet用作卷、守护进程日志的文件系统 当imagefs用量到达驱逐阈值，Kubelet会删除所有未使用的镜像，释放空间。 当nodefs用量到达阈值，Kubelet会选择性的驱逐Pod（及其容器）来释放空间。  本地临时存储触发的驱逐 较新版本的K8S支持设置每个Pod可以使用的临时存储的request/limit，驱逐行为可以更具有针对性。 如果Pod使用了超过限制的本地临时存储，Kubelet将设置驱逐信号，触发Pod驱逐流程： 对于容器级别的隔离，如果一个容器的可写层、日志占用磁盘超过限制，则Kubelet标记Pod为待驱逐 对于Pod级别的隔离，Pod总用量限制，是每个容器限制之和。如果各容器用量之和+Pod的emptyDir卷超过Pod总用量限制，标记Pod为待驱逐 从编排层限制 <a class="read-more" href="https://blog.gmem.cc/limit-disk-usage-for-pods">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/limit-disk-usage-for-pods">限制Pod磁盘空间用量</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">Pod如何使用磁盘</span></div>
<p>容器在运行期间会产生临时文件、日志。如果没有任何配额机制，则某些容器可能很快将磁盘写满，影响宿主机内核和所有应用。</p>
<p>容器的<span style="background-color: #c0c0c0;">临时存储，例如emptyDir</span>，位于目录/var/lib/kubelet/pods下：</p>
<pre class="crayon-plain-tag">/var/lib/kubelet/pods/
└── ac0810f5-a1ce-11ea-9caf-00e04c687e45  # POD_ID
    ├── containers
    │   ├── istio-init
    │   │   └── 32390fd7
    │   ├── istio-proxy
    │   │   └── 70ed81da
    │   └── zookeeper
    │       └── e9e21e59
    ├── etc-hosts          # 命名空间的Host文件
    └── volumes            # Pod的卷
        ├── kubernetes.io~configmap  # ConfigMap类型的卷
        │   └── istiod-ca-cert
        │       └── root-cert.pem -&gt; ..data/root-cert.pem
        ├── kubernetes.io~downward-api
        │   └── istio-podinfo
        │       ├── annotations -&gt; ..data/annotations
        │       └── labels -&gt; ..data/labels
        ├── kubernetes.io~empty-dir # Empty类型的卷
        │   ├── istio-data
        │   └── istio-envoy
        │       ├── envoy-rev0.json
        │       └── SDS
        ├── kubernetes.io~rbd       # RBD卷 
        │   └── pvc-644a7e30-845e-11ea-a4e1-70e24c686d29 # /dev/rbd0挂载到这个挂载点
        ├── kubernetes.io~csi       # CSI卷
        └── kubernetes.io~secret    # Secret类型的卷
            └── default-token-jp4n8
                ├── ca.crt -&gt; ..data/ca.crt
                ├── namespace -&gt; ..data/namespace
                └── token -&gt; ..data/token</pre>
<p><span style="background-color: #c0c0c0;">持久卷的挂载点</span>也位于/var/lib/kubelet/pods下，但是<span style="background-color: #c0c0c0;">不会导致存储空间的消耗</span>。</p>
<p>容器的<span style="background-color: #c0c0c0;">日志，存放在/var/log/pods</span>目录下。</p>
<p>使用Docker时，<span style="background-color: #c0c0c0;">容器的rootfs</span>位于/var/lib/docker下，具体位置取决于存储驱动。</p>
<div class="blog_h1"><span class="graybg">Pod驱逐机制</span></div>
<div class="blog_h2"><span class="graybg">磁盘容量不足触发的驱逐</span></div>
<p>具体细节参考：<a href="/kubernetes-study-note#out-of-resource">/kubernetes-study-note#out-of-resource</a>。</p>
<p>当不可压缩资源（内存、磁盘）不足时，节点上的Kubelet会尝试驱逐掉某些Pod，以释放资源，防止整个系统受到影响。</p>
<p>其中，磁盘资源不足的信号来源有两个：</p>
<ol>
<li>imagefs：容器运行时用作存储镜像、可写层的文件系统</li>
<li>nodefs：Kubelet用作卷、守护进程日志的文件系统</li>
</ol>
<p>当imagefs用量到达驱逐阈值，Kubelet会删除所有未使用的镜像，释放空间。</p>
<p>当nodefs用量到达阈值，Kubelet会选择性的驱逐Pod（及其容器）来释放空间。 </p>
<div class="blog_h2"><span class="graybg">本地临时存储触发的驱逐</span></div>
<p>较新版本的K8S支持设置每个Pod可以使用的临时存储的request/limit，驱逐行为可以更具有针对性。</p>
<p>如果Pod使用了超过限制的本地临时存储，Kubelet将设置驱逐信号，触发Pod驱逐流程：</p>
<ol>
<li>对于容器级别的隔离，如果一个容器的可写层、日志占用磁盘超过限制，则Kubelet标记Pod为待驱逐</li>
<li>对于Pod级别的隔离，Pod总用量限制，是每个容器限制之和。如果各容器用量之和+Pod的emptyDir卷超过Pod总用量限制，标记Pod为待驱逐</li>
</ol>
<div class="blog_h1"><span class="graybg">从编排层限制</span></div>
<p>从K8S 1.8开始，支持本地临时存储（local ephemeral storage），ephemeral的意思是，数据的持久性（durability）不做保证。临时存储可能Backed by 本地Attach的可写设备，或者内存。</p>
<p>Pod可以使用本地临时存储来作为暂存空间，或者存放缓存、日志。Kubelet可以利用本地临时存储，将<span style="background-color: #c0c0c0;">emptyDir卷</span>挂载给容器。Kubelet也使用<span style="background-color: #c0c0c0;">本地临时存储来保存节点级别的容器日志、容器镜像、容器的可写层</span>。</p>
<p>Kubelet会将日志写入到你配置好的日志目录，默认<pre class="crayon-plain-tag">/var/log</pre>。其它文件默认都写入到<pre class="crayon-plain-tag">/var/lib/kubelet</pre>。在典型情况下，这两个目录可能都位于宿主机的rootfs之下。</p>
<p>Kubernetes支持跟踪、保留/限制Pod能够使用的本地临时存储的总量。</p>
<div class="blog_h2"><span class="graybg">限制Pod用量</span></div>
<p>打开特性开关：<pre class="crayon-plain-tag">LocalStorageCapacityIsolation</pre>，可以限制每个Pod能够使用的临时存储的总量。</p>
<p>注意：以内存为媒介（tmpfs）的emptyDir，其用量计入容器内存消耗，而非本地临时存储消耗。</p>
<p>使用类似限制内存、CPU用量的方式，限制本地临时存储用量：</p>
<pre class="crayon-plain-tag">spec.containers[].resources.limits.ephemeral-storage
spec.containers[].resources.requests.ephemeral-storage</pre>
<p>单位可以是E, P, T, G, M, K，或者Ei, Pi, Ti, Gi, Mi, Ki（1024）。</p>
<p>下面这个例子，Pod具有两个容器，每个容器最多使用4GiB的本地临时存储：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: frontend
spec:
  containers:
  - name: db
    image: mysql
    env:
    - name: MYSQL_ROOT_PASSWORD
      value: "password"
    resources:
      requests:
        ephemeral-storage: "2Gi"
      limits:
        ephemeral-storage: "4Gi"
  - name: wp
    image: wordpress
    resources:
      requests:
        ephemeral-storage: "2Gi"
      limits:
        ephemeral-storage: "4Gi"</pre>
<div class="blog_h2"><span class="graybg">对Pod用量的监控</span></div>
<div class="blog_h3"><span class="graybg">不监控</span></div>
<p>如果禁用Kubelet对本地临时存储的监控，则Pod超过limit限制后不会被驱逐。但是，如果磁盘整体上容量太低，节点会被打上污点，所有不能容忍此污点的Pod都会被驱逐。</p>
<div class="blog_h3"><span class="graybg">周期性扫描</span></div>
<p>Kubelet可以执行周期性的扫描，检查emptyDir卷、容器日志目录、可写容器层，然后计算Pod/容器使用了多少磁盘。</p>
<p>这个模式下有个问题需要注意，Kubelet<span style="background-color: #c0c0c0;">不会跟踪已删除文件的描述符</span>。也就是说，如果你创建一个文件，打开文件，写入1GB，然后删除文件，这种情况下inode仍然存在（直到你关闭文件），空间仍然被占用，但是Kubelet却没有算这1GB.</p>
<div class="blog_h3"><span class="graybg">Project Quotas</span></div>
<p>此特性在1.15+处于Alpha状态。</p>
<p>Project quotas是Linux操作系统级别的特性，用于在目录级别限制磁盘用量。只有本地临时存储（例如emptyDir）的后备（Backing）文件系统支持Project quotas，才可以使用该特性。XFS、ext4都支持Project quotas。</p>
<p>K8S将占用从1048576开始的Project ID，占用中的ID注册在/etc/projects、/etc/projid文件中。如果系统中其它进程占用Project ID，则也必须在这两个文件中注册，这样K8S才会改用其它ID。</p>
<p>Quotas比周期性扫描快，而且更加精准。当一个目录被分配到一个Project中后，该目录中创建的任何文件，都是在Project中创建的。为了统计用量，内核只需要跟踪Project中创建了多少block就可以了。</p>
<p>如果文件被创建、然后删除，但是它的文件描述符仍然处于打开状态，这种情况下，它仍然消耗空间，不会出现周期性扫描的那种漏统计的问题。</p>
<p>要启用Project Quotas，你需要：</p>
<ol>
<li>开启Kubelet特性开关：<pre class="crayon-plain-tag">LocalStorageCapacityIsolationFSQuotaMonitoring</pre></li>
<li>确保文件系统支持Project quotas：
<ol>
<li>XFS文件系统默认支持，不需要操作</li>
<li>ext4文件系统，你需要在未挂载之前，启用：<br />
<pre class="crayon-plain-tag">sudo tune2fs -O project -Q prjquota /dev/vda </pre>
</li>
</ol>
</li>
<li>确保文件系统挂载时，启用了Project quotas。使用挂载选项<pre class="crayon-plain-tag">prjquota</pre></li>
</ol>
<div class="blog_h2"><span class="graybg">inode耗尽问题</span></div>
<p>有的时候，我们会发现<span style="background-color: #c0c0c0;">磁盘写入时会报磁盘满</span>，但是<pre class="crayon-plain-tag">df</pre>查看容量并没有100%使用，此时可能只是因为inode耗尽造成的。</p>
<p>当前k8s并不支持对Pod的临时存储设置inode的limits/requests。</p>
<p>但是，如果node进入了inode紧缺的状态，kubelet会将node设置为 under pressure，不再接收新的Pod请求。</p>
<div class="blog_h1"><span class="graybg">从容器引擎限制</span></div>
<div class="blog_h2"><span class="graybg">Docker</span></div>
<p>Docker提供了配置项 <pre class="crayon-plain-tag">--storage-opt</pre>，可以限制容器占用磁盘空间的大小，此大小影响镜像和容器文件系统，默认10G。</p>
<p>你也可以在/etc/docker/daemon.json中修改此配置项：</p>
<pre class="crayon-plain-tag">{
    "storage-driver": "devicemapper",
    "storage-opts": [
        // devicemapper
        "dm.basesize=20G",
        // overlay2
        "overlay2.size=20G",
    ]
}</pre>
<p>但是这种配置无法影响那些挂载的卷，例如emptyDir。</p>
<div class="blog_h1"><span class="graybg">从系统层限制</span></div>
<p>你可以使用Linux系统提供的任何能够限制磁盘用量的机制，为了和K8S对接，需要开发Flexvolume或CSI驱动。</p>
<div class="blog_h2"><span class="graybg">磁盘配额</span></div>
<p>前文已经介绍过，K8S目前支持基于Project quotas来统计Pod的磁盘用量。这里简单总结一下Linux磁盘配额机制。</p>
<div class="blog_h3"><span class="graybg">配额目标</span></div>
<p>Linux系统支持以下几种角度的配额：</p>
<ol>
<li>在文件系统级别，限制群组能够使用的最大磁盘额度</li>
<li>在文件系统级别，限制单个用户能够使用的最大磁盘额度</li>
<li>限制某个目录（directory, project）能够占用的最大磁盘额度</li>
</ol>
<p>前面2种配额，现代Linux都支持，不需要前提条件。你甚至可以在一个虚拟的文件系统上进行配额：</p>
<pre class="crayon-plain-tag"># 写一个空白文件
dd if=/dev/zero of=/path/to/the/file bs=4096 count=4096
# 格式化
...
# 挂载为虚拟文件系统
mount -o loop,rw,usrquota,grpquota /path/to/the/file /path/of/mount/point

# 进行配额设置...</pre>
<p>第3种需要较新的文件系统，例如XFS、ext4fs。</p>
<div class="blog_h3"><span class="graybg">配额角度</span></div>
<p>配额可以针对Block用量进行，也可以针对inode用量进行。</p>
<p>配额可以具有软限制、硬限制。超过软限制后，仍然可以正常使用，但是登陆后会收到警告，在grace time倒计时完毕之前，用量低于软限制后，一切恢复正常。如果grace time到期仍然没做清理，则无法创建新文件。</p>
<div class="blog_h3"><span class="graybg">统计用量</span></div>
<p>启用配额，内核自然需要统计用量。管理员要查询用量，可以使用<pre class="crayon-plain-tag">xfs_quota</pre>这样的命令，比du这种遍历文件计算的方式要快得多。</p>
<div class="blog_h3"><span class="graybg">启用配额</span></div>
<p>在保证底层文件系统支持之后，你需要修改<span style="background-color: #c0c0c0;">挂载选项</span>来启用配额：</p>
<ol>
<li>uquota/usrquota/quota：针对用户设置配额</li>
<li>gquota/grpquota：针对群组设置配额</li>
<li>pquota/prjquota：针对目录设置配额</li>
</ol>
<div class="blog_h2"><span class="graybg">LVM</span></div>
<p>使用LVM你可以任意创建<span style="background-color: #c0c0c0;">具有尺寸限制的逻辑卷</span>，把这些逻辑卷挂载给Pod即可：</p>
<pre class="crayon-plain-tag">volumes:
- flexVolume:
    # 编写的flexVolume驱动放到
    # /usr/libexec/kubernetes/kubelet-plugins/volume/exec/kubernetes.io~lvm/lvm
    driver: kubernetes.io/lvm
    fsType: ext4
    options:
      size: 30Gi
      volumegroup: docker
  name: mnt
volumeMounts:
  - mountPath: /mnt
    name: mnt </pre>
<p>这需要修改编排方式，不使用emptyDir这种本地临时存储，还需要处理好逻辑卷清理工作。</p>
<p>Flexvolume驱动的示例可以参考：<a href="/flexvolume-study-note#lvm">/flexvolume-study-note#lvm</a>。</p>
<div class="blog_h2"><span class="graybg">DeviceMapper</span></div>
<p>使用Device Mapper也可以创建具有尺寸限制的卷，比起LVM的优势是thin-provisioning，不必预先分配空间。</p>
<p>DM是从2.6引入内核的通用设备映射机制，LVM就是基于DM的。DM为<span style="background-color: #c0c0c0;">块设备驱动提供了模块化的内核架构</span>。</p>
<div class="blog_h3"><span class="graybg">基本概念</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Mapped Device</td>
<td>
<p>由内核映射出的、逻辑的设备，它和Target Device的关系，由Mapping Table维护</p>
<p>Mapped Device可以：</p>
<ol>
<li>映射到一个Target Device</li>
<li>映射到多个Target Device</li>
<li>映射到另外一个Mapped Device</li>
</ol>
</td>
</tr>
<tr>
<td>Mapping Table</td>
<td>包含字段：Mapped Device的逻辑起始地址、范围、关联Target Device所在物理设备的地址偏移量（以扇区，512字节为单位）、Target类型，等等</td>
</tr>
<tr>
<td>Target Device</td>
<td>Mapped Device所映射的物理空间段</td>
</tr>
<tr>
<td>Target Driver</td>
<td>
<p>在内核中，DM通过模块化的Target Driver插件，实现对IO请求的过滤、重定向等操作。插件的实现包括：软Raid、加密、镜像、快照、Thin Provisioning </p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">命令行</span></div>
<p>使用dmsetup命令，你可以创建、删除虚拟卷，具体参考文章：<a href="/linux-command-faq#dmsetup">Linux命令知识集锦</a>。</p>
<p>下面的例子，以文件为后备，创建虚拟块设备（loopback），并以此块设备为基础，创建thin-provisioning池。然后，在池中分配限制大小的卷：</p>
<pre class="crayon-plain-tag"># 创建空白文件
# 声明大小10G，实际占用空间（seek）4K
dd if=/dev/zero of=/tmp/data.img bs=1K count=1 seek=10M
# 声明大小100M
dd if=/dev/zero of=/tmp/meta.img bs=1K count=1 seek=100K

# 映射为虚拟（loopback）块设备，实际场景中，你可以考虑将整块磁盘交由DM管理
losetup /dev/loop10 /tmp/meta.img
losetup /dev/loop11 /tmp/data.img

# 基于上述块设备，创建一个mapped device（thin-provisioning池）
dmsetup create thinpool0 \
#          数据设备起始扇区
#            数据设备结束扇区 * 512 = 10G      
#                               元数据设备
#                                           数据设备     
  --table "0 20971522 thin-pool /dev/loop10 /dev/loop11 \
# 最小可以分配的扇区数
#    最少可用的扇区阈值
#          有1个附加参数
#            附加参数，跳过用0填充的块 
 128 65536 1 skip_block_zeroing"

# 设备 /dev/mapper/thinpool0 现在可用

# 在thinpool0上创建一个thin-provisioning卷
#                                        标识符
dmsetup message thinpool0 0 "create_thin 0"
dmsetup create thinvol0 --table "0 2097152 thin /dev/mapper/thinpool0 0"

# 设备 /dev/mapper/thinvol0 现在可用

# 格式化
mkfs.ext4 /dev/mapper/thinvol0

# 挂载
mkdir /tmp/thinvol
mount /dev/mapper/thinvol0 /tmp/thinvol</pre>
<div class="blog_h1"><span class="graybg">参考</span></div>
<ol>
<li><a href="https://blog.spider.im/post/control-disk-size-in-docker/">https://blog.spider.im/post/control-disk-size-in-docker</a></li>
<li><a href="https://ieevee.com/tech/2019/05/23/ephemeral-storage.html">https://ieevee.com/tech/2019/05/23/ephemeral-storage.html</a></li>
<li><a href="https://wizardforcel.gitbooks.io/vbird-linux-basic-4e/content/125.html">https://wizardforcel.gitbooks.io/vbird-linux-basic-4e/content/125.html</a></li>
<li><a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#local-ephemeral-storage">https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#local-ephemeral-storage</a></li>
<li><a href="https://coolshell.cn/articles/17200.html">https://coolshell.cn/articles/17200.html</a></li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/limit-disk-usage-for-pods">限制Pod磁盘空间用量</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/limit-disk-usage-for-pods/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
