
# 长连接业务配置ELB Ingress负载均衡最佳实践
当长连接业务容器通过ELB Ingress对外提供访问时，可能会遇到压测业务不均衡的问题，导致部分服务器负载过高，影响业务性能。为了解决这一问题，您可以配置客户端连接空闲超时时间、等待客户端请求超时时间、等待后端服务器响应超时时间等参数进行优化，从而实现业务的均衡分布和高效运行。
- **客户端连接空闲超时时间**：该配置决定了在没有数据传输的情况下，连接可以保持多久。对于长连接来说，设置一个合理的空闲超时时间可以避免连接长时间占用资源，同时确保在需要时连接仍然可用。如果设置得太短，可能会导致频繁的连接断开和重新建立，增加服务器的负担；如果设置得太长，则可能导致资源浪费。
- **等待客户端请求超时时间**：该配置决定了服务器在接收到客户端请求后，等待客户端发送完整请求的时间。对于长连接，这个设置可以防止服务器无限期等待客户端发送数据，从而避免资源被长时间占用。合理设置这个超时时间，可以提高服务器的响应效率和资源利用率。
- **等待后端服务器响应超时时间**：该配置决定了服务器在向后端服务器发送请求后，等待后端服务器响应的时间。对于长连接，合理设置这个超时时间可以避免因后端服务器响应慢而导致的连接阻塞，从而提高系统的整体性能和稳定性。
#### 前提条件
- 创建一个CCE Turbo集群，并安装云原生监控插件。
- 创建一个独享型ELB。
- 创建一台可访问公网的ECS主机，并安装Docker和wrk压测工具。
 
#### 步骤一：准备测试镜像
1. 登录ECS主机，创建一个dockerfile文件夹。 
   ```
   mkdir ./dockerfile
   cd ./dockerfile
   ```
   
   
2. 准备本文中构建测试镜像所需的三个文件：Dockerfile、go.mod、app.go。 
   示例应用文件如下
   - Dockerfile内容如下：
     ```
     FROM golang:1.19-alpine AS builder
      
     WORKDIR /app
      
     # 复制go模块文件
     COPY go.mod ./
     # 下载依赖
     RUN go mod download
      
     # 复制源代码
     COPY *.go ./
      
     # 构建应用
     RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o http-long-conn .
      
     # 运行阶段
     FROM alpine:3.10.2
      
     RUN apk --no-cache add ca-certificates
      
     WORKDIR /root/
      
     # 从构建阶段复制二进制文件
     COPY --from=builder /app/http-long-conn .
      
     # 暴露端口
     EXPOSE 8080
      
     # 设置环境变量
     ENV PORT=8080
      
     # 运行服务
     CMD ["./http-long-conn"]
     ```
     
   
   - go.mod文件内容如下：
     ```
     module http-long-conn
      
     go 1.19
     ```
     
   
   - app.go文件内容如下：
     ```
     package main
      
     import (
                              "encoding/json"
                              "fmt"
                              "log"
                              "net/http"
                              "os"
                              "sync"
                              "time"
     )
      
     var (
                              activeConnections int
                              mu                sync.RWMutex
                              startTime         = time.Now()
                              totalRequests     int64
     )
      
     type StatsResponse struct {
                              Status            string `json:"status"`
                              ActiveConnections int    `json:"active_connections"`
                              TotalRequests     int64  `json:"total_requests"`
                              Uptime            string `json:"uptime"`
                              Hostname          string `json:"hostname"`
                              Message           string `json:"message"`
     }
      
     type ConnectionRequest struct {
                              ClientID  string `json:"client_id"`
                              KeepAlive int    `json:"keep_alive"` // 保持连接的时间（秒）
     }
      
     type ConnectionResponse struct {
                              Status       string `json:"status"`
                              ConnectionID string `json:"connection_id"`
                              ServerTime   string `json:"server_time"`
                              KeepAlive    int    `json:"keep_alive"`
                              Message      string `json:"message"`
     }
      
     func main() {
                              port := os.Getenv("PORT")
                              if port == "" {
                                                        port = "8080"
                              }
      
                              http.HandleFunc("/", rootHandler)
                              http.HandleFunc("/health", healthHandler)
                              http.HandleFunc("/stats", statsHandler)
                              http.HandleFunc("/long-connection", longConnectionHandler)
                              http.HandleFunc("/ws", websocketHandler)
      
                              log.Printf("HTTP长连接服务启动在端口 %s", port)
                              log.Printf("可用端点:")
                              log.Printf("  GET  /              - 根路径")
                              log.Printf("  GET  /health        - 健康检查")
                              log.Printf("  GET  /stats         - 服务统计")
                              log.Printf("  POST /long-connection - 建立长连接")
                              log.Printf("  GET  /ws            - WebSocket连接")
      
                              if err := http.ListenAndServe(":"+port, nil); err != nil {
                                                        log.Fatal("服务器启动失败:", err)
                              }
     }
      
     func rootHandler(w http.ResponseWriter, r *http.Request) {
                              mu.Lock()
                              totalRequests++
                              mu.Unlock()
      
                              hostname, _ := os.Hostname()
                              response := map[string]interface{}{
                                                        "service":   "HTTP长连接测试服务",
                                                        "version":   "1.0.0",
                                                        "hostname":  hostname,
                                                        "timestamp": time.Now().Format(time.RFC3339),
                                                        "endpoints": []string{
                                                                                 "GET  /health",
                                                                                 "GET  /stats",
                                                                                 "POST /long-connection",
                                                                                 "GET  /ws",
                                                        },
                              }
      
                              w.Header().Set("Content-Type", "application/json")
                              json.NewEncoder(w).Encode(response)
     }
      
     func healthHandler(w http.ResponseWriter, r *http.Request) {
                              response := map[string]string{
                                                        "status":    "healthy",
                                                        "timestamp": time.Now().Format(time.RFC3339),
                              }
      
                              w.Header().Set("Content-Type", "application/json")
                              json.NewEncoder(w).Encode(response)
     }
      
     func statsHandler(w http.ResponseWriter, r *http.Request) {
                              mu.RLock()
                              defer mu.RUnlock()
      
                              hostname, _ := os.Hostname()
                              response := StatsResponse{
                                                        Status:            "running",
                                                        ActiveConnections: activeConnections,
                                                        TotalRequests:     totalRequests,
                                                        Uptime:            time.Since(startTime).String(),
                                                        Hostname:          hostname,
                                                        Message:           "HTTP长连接服务运行中",
                              }
      
                              w.Header().Set("Content-Type", "application/json")
                              json.NewEncoder(w).Encode(response)
     }
      
     func longConnectionHandler(w http.ResponseWriter, r *http.Request) {
                              mu.Lock()
                              activeConnections++
                              totalRequests++
                              mu.Unlock()
      
                              defer func() {
                                                        mu.Lock()
                                                        activeConnections--
                                                        mu.Unlock()
                              }()
      
                              // 设置长连接头
                              w.Header().Set("Content-Type", "application/json")
                              w.Header().Set("Connection", "keep-alive")
      
                              var req ConnectionRequest
                              if r.Method == "POST" {
                                                        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
                                                                                 http.Error(w, `{"error": "无效的请求体"}`, http.StatusBadRequest)
                                                                                 return
                                                        }
                              }
      
                              // 默认保持30秒连接
                              keepAlive := 30
                              if req.KeepAlive > 0 {
                                                        keepAlive = req.KeepAlive
                              }
      
                              clientID := req.ClientID
                              if clientID == "" {
                                                        clientID = fmt.Sprintf("client-%d", time.Now().UnixNano())
                              }
      
                              // 创建响应
                              response := ConnectionResponse{
                                                        Status:       "connected",
                                                        ConnectionID: fmt.Sprintf("conn-%d", time.Now().UnixNano()),
                                                        ServerTime:   time.Now().Format(time.RFC3339),
                                                        KeepAlive:    keepAlive,
                                                        Message:      "长连接已建立，将保持连接",
                              }
      
                              // 发送初始响应
                              if err := json.NewEncoder(w).Encode(response); err != nil {
                                                        log.Printf("发送初始响应失败: %v", err)
                                                        return
                              }
      
                              // 刷新缓冲区，确保数据发送到客户端
                              if flusher, ok := w.(http.Flusher); ok {
                                                        flusher.Flush()
                              }
      
                              // 保持连接，定期发送心跳
                              ticker := time.NewTicker(5 * time.Second)
                              defer ticker.Stop()
      
                              timeout := time.After(time.Duration(keepAlive) * time.Second)
      
                              for {
                                                        select {
                                                        case <-ticker.C:
                                                                                 // 发送心跳
                                                                                 heartbeat := map[string]interface{}{
                                                                                                           "type":      "heartbeat",
                                                                                                           "timestamp": time.Now().Format(time.RFC3339),
                                                                                                           "client_id": clientID,
                                                                                 }
      
                                                                                 // 发送换行符分隔的JSON
                                                                                 if _, err := w.Write([]byte("\n")); err != nil {
                                                                                                           log.Printf("发送换行符失败: %v", err)
                                                                                                           return // 客户端断开连接
                                                                                 }
      
                                                                                 if err := json.NewEncoder(w).Encode(heartbeat); err != nil {
                                                                                                           log.Printf("发送心跳失败: %v", err)
                                                                                                           return // 客户端断开连接
                                                                                 }
      
                                                                                 if flusher, ok := w.(http.Flusher); ok {
                                                                                                           flusher.Flush()
                                                                                 }
      
                                                        case <-timeout:
                                                                                 // 连接超时
                                                                                 endMsg := map[string]string{
                                                                                                           "type":    "connection_end",
                                                                                                           "message": "连接保持时间到期",
                                                                                 }
                                                                                 json.NewEncoder(w).Encode(endMsg)
                                                                                 return
      
                                                        case <-r.Context().Done():
                                                                                 // 客户端断开连接
                                                                                 log.Printf("客户端断开连接: %s", clientID)
                                                                                 return
                                                        }
                              }
     }
      
     func websocketHandler(w http.ResponseWriter, r *http.Request) {
                              // 简单的WebSocket兼容响应
                              w.Header().Set("Content-Type", "application/json")
                              json.NewEncoder(w).Encode(map[string]string{
                                                        "error":      "WebSocket端点需要额外实现，当前使用HTTP长连接",
                                                        "suggestion": "使用/long-connection端点进行HTTP长连接测试",
                              })
     }
     ```
     
    
   
   
3. 创建完成后，查看dockerfile目录下包含以下文件。 
   ```
   app.go  Dockerfile  go.mod
   ```
   
   
4. 构建镜像。
   
   ```
   docker build -t http-long-conn:v1 .
   ```
   等待镜像构建完成。
   ```
   ...
   Successfully built fdc357e8247d
   Successfully tagged http-long-conn:v1
   ```
   
   
5. 登录SWR镜像仓库，将构建的镜像推送到SWR仓库。详情请参见[推送镜像到镜像仓库](https://support.huaweicloud.com/usermanual-swr/swr_01_0011.html)。
   
   ```
   docker tag http-long-conn:v1 {Image repository address}/{Organization}/http-long-conn:v1
   docker push {Image repository address}/{Organization}/http-long-conn:v1
   ```
   其中{Image repository address}为SWR镜像仓库地址，{Organization}为SWR组织名称。
   
   
 
#### 步骤二：在集群中部署工作负载
1. [使用kubectl连接集群](https://support.huaweicloud.com/usermanual-cce/cce_10_0107.html)。
2. 创建一个名为http-long-conn.yaml的文件。其中，http-long-conn.yaml为自定义名称，您可以随意命名。 
   ```
   vi http-long-conn.yaml
   ```
   文件示例如下：
   ```
   kind: Deployment
   apiVersion: apps/v1
   metadata:
     name: http-long-conn
     namespace: default
     labels:
       app: http-long-conn
   spec:
     replicas: 4
     selector:
       matchLabels:
         app: http-long-conn
     template:
       metadata:
         labels:
           app: http-long-conn
       spec:
         containers:
           - name: http-long-conn
             image: {Image repository address}/{Organization}/http-long-conn:v1 #替换为您上传的SWR镜像地址
             ports:
               - containerPort: 8080
                 protocol: TCP
             env:
               - name: PORT
                 value: '8080'
             resources:
               limits:
                 cpu: 100m
                 memory: 128Mi
               requests:
                 cpu: 100m
                 memory: 128Mi
             livenessProbe:
               httpGet:
                 path: /health
                 port: 8080
                 scheme: HTTP
               initialDelaySeconds: 30
               timeoutSeconds: 1
               periodSeconds: 10
               successThreshold: 1
               failureThreshold: 3
             readinessProbe:
               httpGet:
                 path: /health
                 port: 8080
                 scheme: HTTP
               initialDelaySeconds: 5
               timeoutSeconds: 1
               periodSeconds: 5
               successThreshold: 1
               failureThreshold: 3
             terminationMessagePath: /dev/termination-log
             terminationMessagePolicy: File
             imagePullPolicy: IfNotPresent
         restartPolicy: Always
         terminationGracePeriodSeconds: 30
         dnsPolicy: ClusterFirst
         securityContext: {}
         schedulerName: default-scheduler
         imagePullSecrets:
           - name: default-secret
         tolerations: null
     strategy:
       type: RollingUpdate
       rollingUpdate:
         maxUnavailable: 25%
         maxSurge: 25%
     revisionHistoryLimit: 10
     progressDeadlineSeconds: 600
   ---
   apiVersion: v1
   kind: Service
   metadata:
     name: http-long-conn
     labels:
       app: http-long-conn
     namespace: default
   spec:
     selector:
       app: http-long-conn
     ports:
       - name: http-0
         targetPort: 8080
         nodePort: 0
         port: 8080
         protocol: TCP
     type: ClusterIP
   ```
   
   
3. 创建工作负载及服务。 
   ```
   kubectl create -f http-long-conn.yaml
   ```
   
   
4. 查看工作负载状态。 
   ```
   kubectl get pod -l app=http-long-conn
   ```
   ![](https://support.huaweicloud.com/bestpractice-cce/zh-cn_image_0000002499552438.png "点击放大")
   
   
 
#### 步骤四：创建ELB Ingress
1. 创建一个名为elb-ingress.yaml的文件。其中，elb-ingress.yaml为自定义名称，您可以随意命名。 
   ```
   vi elb-ingress.yaml
   ```
   文件示例如下：
   ```
   apiVersion: networking.k8s.io/v1
   kind: Ingress
   metadata:
     name: http-long-conn
     namespace: default
     annotations:
       kubernetes.io/elb.port: '8080'
       kubernetes.io/elb.id: 1fce4b38-c72b-4fd4-8430-62d46c0a7998   #ELB ID
       kubernetes.io/elb.class: performance
       kubernetes.io/elb.keepalive_timeout: '300'  # 客户端连接空闲超时时间
       kubernetes.io/elb.client_timeout: '60'      # 等待客户端请求超时时间
       kubernetes.io/elb.member_timeout: '60'      # 等待后端服务器响应超时时间
   spec:
     rules:
       - host: example.com  #自定义域名
         http:
           paths:
             - path: /
               backend:
                 service:
                   name: http-long-conn
                   port:
                     number: 8080
               property:
                 ingress.beta.kubernetes.io/url-match-mode: STARTS_WITH
               pathType: ImplementationSpecific
     ingressClassName: cce
   ```
   
   
2. 创建Ingress。 
   ```
   kubectl create -f elb-ingress.yaml
   ```
   
   
3. 测试访问域名。 
   ![](https://support.huaweicloud.com/bestpractice-cce/zh-cn_image_0000002531246349.png "点击放大")
   
   
 
#### 步骤五：压测
使用wrk工具进行压测：
```
wrk -t2 -c100 -d300s -H "Connection: keep-alive" http://example.com/long-connection
```
参数说明：
- -t2：表示启用2个线程
- -c100：设置100个并发连接
- -d300s：定义测试持续时间
![](https://support.huaweicloud.com/bestpractice-cce/zh-cn_image_0000002499407090.png "点击放大")
本文示例中将并发连接配置100和200分别进行压测，在监控中心查看对应的Pod监控数据，基本处于负载均衡的状态。
![](https://support.huaweicloud.com/bestpractice-cce/zh-cn_image_0000002531411201.png "点击放大")
