Featured image of post 跟我一起玩微服务网关

跟我一起玩微服务网关

微服务网关实战:Go + Consul + go-kit + Kong + JWT + Zipkin

本文是个人在学习书籍《Go 语言高并发与微服务实战》第9章内容过程中,动手做的小实验,涉及 Consul、go-kit、Kong、JWT、Zipkin 相关知识。

微服务网关概述

┌─────────────────────────────────────────────────────────────┐
│                     微服务网关架构                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌──────────────────────────────────────────────────────┐ │
│   │                     API Gateway                      │ │
│   ├──────────────────────────────────────────────────────┤ │
│   │  路由转发 │ 负载均衡 │ 限流熔断 │ 认证鉴权 │ 监控日志 │ │
│   └──────────────────────────────────────────────────────┘ │
│                              │                              │
│       ┌──────────┬───────────┼───────────┬──────────┐      │
│       │          │           │           │          │      │
│       ▼          ▼           ▼           ▼          ▼      │
│   ┌─────────┐┌─────────┐┌─────────┐┌─────────┐┌─────────┐  │
│   │User Svc ││Order Svc││Product  ││Payment  ││Search   │  │
│   │         ││         ││Service  ││Service  ││Service  │  │
│   └─────────┘└─────────┘└─────────┘└─────────┘└─────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

网关核心功能

功能说明
路由转发根据请求路径转发到后端服务
负载均衡多实例流量分发
限流熔断保护后端服务
认证鉴权JWT/Session 验证
监控日志请求追踪和日志记录

简易网关 Go 实现

项目结构

gateway/
├── main.go           # 主程序
├── proxy.go          # 反向代理
├── discovery.go      # 服务发现
├── limiter.go       # 限流器
├── circuitbreaker.go # 熔断器
└── auth.go           # 认证中间件

主程序

// gateway/main.go
package main

import (
    "flag"
    "fmt"
    "log"
    "math/rand"
    "net/http"
    "net/http/httputil"
    "os"
    "os/signal"
    "strings"
    "syscall"
    "time"
    
    "github.com/go-kit/kit/log"
    "github.com/hashicorp/consul/api"
)

func main() {
    // 解析命令行参数
    var (
        consulHost = flag.String("consul.host", "127.0.0.1", "consul server host")
        consulPort = flag.String("consul.port", "8500", "consul server port")
        gatewayPort = flag.String("gateway.port", "9090", "gateway port")
    )
    flag.Parse()
    
    // 创建日志组件
    var logger log.Logger
    {
        logger = log.NewLogfmtLogger(os.Stderr)
        logger = log.With(logger, "ts", log.DefaultTimestampUTC)
        logger = log.With(logger, "caller", log.DefaultCaller)
    }
    
    // 创建 Consul 客户端
    consulConfig := api.DefaultConfig()
    consulConfig.Address = fmt.Sprintf("http://%s:%s", *consulHost, *consulPort)
    consulClient, err := api.NewClient(consulConfig)
    if err != nil {
        logger.Log("err", err)
        os.Exit(1)
    }
    
    // 创建处理器
    discovery := NewDiscovery(consulClient, logger)
    handler := NewProxyHandler(discovery, logger)
    
    // 创建 HTTP 服务器
    server := &http.Server{
        Addr:    fmt.Sprintf(":%s", *gatewayPort),
        Handler: handler,
    }
    
    // 启动服务器
    go func() {
        logger.Log("gateway", "starting", "port", *gatewayPort)
        if err := server.ListenAndServe(); err != nil {
            logger.Log("gateway", "exit", "err", err)
        }
    }()
    
    // 优雅退出
    ch := make(chan os.Signal, 1)
    signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
    logger.Log("signal", <-ch)
    
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    if err := server.Shutdown(ctx); err != nil {
        logger.Log("shutdown", err)
    }
}

服务发现

// gateway/discovery.go
package main

import (
    "context"
    "github.com/go-kit/kit/log"
    "github.com/hashicorp/consul/api"
    "math/rand"
)

type Discovery struct {
    client consul.Client
    logger log.Logger
}

func NewDiscovery(client consul.Client, logger log.Logger) *Discovery {
    return &Discovery{
        client: client,
        logger: logger,
    }
}

// GetService 获取服务实例
func (d *Discovery) GetService(ctx context.Context, serviceName string) (*api.ServiceEntry, error) {
    entries, _, err := d.client.Health().Service(serviceName, "", true, nil)
    if err != nil {
        return nil, err
    }
    
    if len(entries) == 0 {
        return nil, fmt.Errorf("no available instances for %s", serviceName)
    }
    
    // 负载均衡:随机选择
    index := rand.Intn(len(entries))
    return &entries[index], nil
}

// GetServices 获取所有服务实例
func (d *Discovery) GetServices(ctx context.Context, serviceName string) ([]*api.ServiceEntry, error) {
    entries, _, err := d.client.Health().Service(serviceName, "", true, nil)
    if err != nil {
        return nil, err
    }
    return entries, nil
}

反向代理

// gateway/proxy.go
package main

import (
    "context"
    "fmt"
    "github.com/go-kit/kit/log"
    "net/http"
    "net/http/httputil"
    "strings"
)

type ProxyHandler struct {
    discovery *Discovery
    logger    log.Logger
}

func NewProxyHandler(discovery *Discovery, logger log.Logger) *ProxyHandler {
    return &ProxyHandler{
        discovery: discovery,
        logger:    logger,
    }
}

func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // 解析请求路径:/service-name/path
    path := r.URL.Path
    parts := strings.SplitN(path, "/", 3)
    if len(parts) < 3 {
        http.Error(w, "invalid path", http.StatusBadRequest)
        return
    }
    
    serviceName := parts[1]
    targetPath := "/" + parts[2]
    
    // 获取服务实例
    instance, err := h.discovery.GetService(r.Context(), serviceName)
    if err != nil {
        h.logger.Log("service", serviceName, "err", err)
        http.Error(w, "service unavailable", http.StatusServiceUnavailable)
        return
    }
    
    // 构建代理请求
    targetURL := fmt.Sprintf("http://%s:%d%s", 
        instance.Service.Address, 
        instance.Service.Port, 
        targetPath)
    
    proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
    if err != nil {
        http.Error(w, "proxy error", http.StatusInternalServerError)
        return
    }
    
    // 复制请求头
    for key, values := range r.Header {
        for _, value := range values {
            proxyReq.Header.Add(key, value)
        }
    }
    
    // 执行代理
    client := &http.Client{}
    resp, err := client.Do(proxyReq)
    if err != nil {
        h.logger.Log("proxy", err)
        http.Error(w, "proxy error", http.StatusBadGateway)
        return
    }
    defer resp.Body.Close()
    
    // 复制响应
    for key, values := range resp.Header {
        for _, value := range values {
            w.Header().Add(key, value)
        }
    }
    w.WriteHeader(resp.StatusCode)
    w.Write([]byte(fmt.Sprintf("Proxy to: %s", targetURL)))
}

// 反向代理实现
func newSingleHostReverseProxy(target string) *httputil.ReverseProxy {
    return httputil.NewSingleHostReverseProxy(target)
}

Kong 网关

Kong 简介

Kong 是一个云原生、可扩展、统一 API 层(API Gateway)。

Docker 部署

下面给出「单机演示」的常见步骤:创建网络 → 启动数据库 → (按所用 Kong 镜像文档执行)数据库迁移 → 启动 Kong。具体镜像名与启动参数请以你选用的 Kong 发行版 README 为准,不同主版本 CLI 与环境变量略有差异。

docker network create kong-net

docker run -d --name kong-db \
  --network kong-net \
  -p 5432:5432 \
  -e POSTGRES_USER=kong \
  -e POSTGRES_PASSWORD=kong \
  -e POSTGRES_DB=kong \
  postgres:13

# 数据库就绪后,在同一网络内执行 kong migrations bootstrap(命令随镜像而异),再启动 kong 网关与管理端口。
# 典型端口:8000(代理)、8443(HTTPS)、8001(Admin API)。正式环境请改用编排模板并开启 TLS / RBAC。

本地体验也可使用 Kong Ingress Controller(Kubernetes)或云端托管网关服务;本节重点在于:网关前面负责 TLS、路由、插件(鉴权、限流、熔断),后面的微服务仍可沿用前文自定义代理或 Istio Linkerd 等服务网格。

小结

网关层把「路由、观测、防护」收口在一处:开发阶段可用 Nginx / 手写反向代理入门,业务量上升后再引入 Kong / APISIX / Envoy 等平台化能力。

© 2016-2026 Yison. All rights reserved.
使用 Hugo 构建
主题 StackJimmy 设计