美文网首页
Kong中使用grpc-web插件代理grpc服务时遇到的坑

Kong中使用grpc-web插件代理grpc服务时遇到的坑

作者: 乱七八糟谈技术 | 来源:发表于2020-07-28 08:46 被阅读0次

    在大型分布式系统中,有很多的微服务对外提供服务,也会有各种微服务的协议需要集成,比如http,https,grpc的,这时就需要一个API网关提供高性能、高可用的API托管服务,帮助服务的开发者便捷地对外提供服务,而不用考虑安全控制、流量控制、审计日志等问题,统一在网关层将安全认证,流量控制,审计日志,黑白名单等实现。网关的下一层,是内部服务,内部服务只需开发和关注具体业务相关的实现。网关可以提供API发布、管理、维护等主要功能。开发者只需要简单的配置操作即可把自己开发的服务发布出去,同时置于网关的保护之下。我们项目中使用的API Gateway是Kong,在代理grpc服务的时候,我们需要将此grpc服务以http/https协议提供给前端访问,因此需要用到Kong提供的grpc-web插件来帮忙将http/https的请求代理到后端的grpc服务上,在kong-plugin-grpc-web的官网上提供了配置实例,但按照此实例配置没法代理成功,而且基本网上都没有找到其他demo和资料,解决过程很简单,就是根据错误信息查看源码,分析出实际项目中该如何使用grpc-web插件来配置grpc服务。

    关于Kong

    Kong是一款基于Nginx_Lua模块写的高可用,易扩展由Mashape公司开源的API Gateway项目。由于Kong是基于Nginx的,所以可以水平扩展多个Kong服务器,通过前置的负载均衡配置把请求均匀地分发到各个Server,来应对大批量的网络请求。Kong采用插件机制进行功能定制,插件集(可以是0或n个)在API请求响应循环的生命周期中被执行。插件使用Lua编写,目前已有几个基础功能:HTTP基本认证、密钥认证、CORS( Cross-origin Resource Sharing,跨域资源共享)、TCP、UDP、文件日志、API请求限流、请求转发以及nginx监控。这篇文章中我们会用到另一个开源的插件,官网地址:

    https://github.com/Kong/kong-plugin-grpc-web

    关于Kong gRPC-Web插件

    A Kong plugin to allow access to a gRPC service via the gRPC-Web protocol. Primarily, this means JS browser apps using the gRPC-Web library.
    A service that presents a gRPC API can be used by clients written in many languages, but the network specifications are oriented primarily to connections within a datacenter. In order to expose the API to the Internet, and to be called from brower-based JS apps, gRPC-Web was developed.This plugin translates requests and responses between gRPC-Web and "real" gRPC. Supports both HTTP/1.1 and HTTP/2, over plaintext (HTTP) and TLS (HTTPS) connections.

    这是来自官网的描述,简单的说就是开发者可以使用任何语言开发gRPC服务,前端js程序需要通过gRPC-Web协议来访问gRPC服务,使用此插件可以实现使用HTTP REST请求来请求后端的gRPC服务,请求和返回数据是json格式。

    Springboot开发grpc服务

    为了测试gRPC代理,使用springboot开发一个简单的gRPC服务。

    第一步,定义proto文件,并放在src/main/proto目录下,命名为HelloWorld.proto

    syntax = "proto3";

    option java_multiple_files = true;
    package com.example.grpc.helloworld;

    message Person {
      string first_name = 1;
      string last_name = 2;
    }

    message Greeting {
      string message = 1;
    }

    service HelloWorldService {
      rpc sayHello (Person) returns (Greeting);}

    第二步,设置Maven

    注意以下几个地方,spring-boot-starter-webprotobuf-maven-pluginbuild-helper-maven-pluginspring-boot-starter-web将自动的使用内置的tomcat来部署grpc服务。protobuf-maven-plugin是用来根据定义的proto文件来生成grpc-based的java代码。build-helper-maven-plugin此插件是用来设置将产生的java source code作为编译的一部分,也能帮助IntellJ工具找到源码,否则虽然编译能成功,但IntellJ会出现很多的红线提示错误,不太友好,而且也不能F3定位到源码的位置。

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.3.1.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.example.grpc</groupId>
        <artifactId>demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>demo</name>
        <description>Demo project for Spring Boot</description>

        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
            <java.version>1.8</java.version>
            <grpc-spring-boot-starter.version>3.0.0</grpc-spring-boot-starter.version>
            <os-maven-plugin.version>1.6.1</os-maven-plugin.version>
            <protobuf-maven-plugin.version>0.6.1</protobuf-maven-plugin.version>
        </properties>

        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>io.github.lognet</groupId>
                <artifactId>grpc-spring-boot-starter</artifactId>
                <version>${grpc-spring-boot-starter.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-log4j2</artifactId>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
        </dependencies>

        <build>
            <extensions>
                <extension>
                    <groupId>kr.motd.maven</groupId>
                    <artifactId>os-maven-plugin</artifactId>
                    <version>${os-maven-plugin.version}</version>
                </extension>
            </extensions>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
                <plugin>
                    <groupId>org.xolstice.maven.plugins</groupId>
                    <artifactId>protobuf-maven-plugin</artifactId>
                    <version>${protobuf-maven-plugin.version}</version>
                    <configuration>
                        <protocArtifact>com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier}</protocArtifact>
                        <pluginId>grpc-java</pluginId>
                        <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.16.1:exe:${os.detected.classifier}</pluginArtifact>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>compile</goal>
                                <goal>compile-custom</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>build-helper-maven-plugin</artifactId>
                    <version>1.4</version>
                    <executions>
                        <execution>
                            <id>test</id>
                            <phase>generate-sources</phase>
                            <goals>
                                <goal>add-source</goal>
                            </goals>
                            <configuration>
                                <sources>
                                    <source>${basedir}/target/generated-sources</source>
                                </sources>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </project>

    第三步,实现grpc服务

    服务实现代码很简单,如下:

    @GRpcService
    @Slf4j
    public class HelloWorldServiceImpl
            extends HelloWorldServiceGrpc.HelloWorldServiceImplBase {

        @Override
        public void sayHello(Person request,                         StreamObserver<Greeting> responseObserver) {
            log.info("server received {}", request);

            String message = "Hello " + request.getFirstName() + " "
                    + request.getLastName() + "!";
            Greeting greeting =
                    Greeting.newBuilder().setMessage(message).build();
            log.info("server responded {}", greeting);

            responseObserver.onNext(greeting);
            responseObserver.onCompleted();
        }
    }

    使用springboot运行此项目,将默认启动6565端口发布此grpc服务。下面我们就使用grpc-web 插件部署此服务

    安装KONG

    第一步,创建Docker网络

    docker network create kong-net

    第二步,安装Postgresql或者Cassandra

    安装Cassandra作为存储

    docker run -d --name kong-database \
                   --network=kong-net \
                   -p 9042:9042 \
                   cassandra:3

    或者安装Postgresql作为存储

    docker run -d --name kong-database \
                   --network=kong-net \
                   -p 5432:5432 \
                   -e "POSTGRES_USER=kong" \
                   -e "POSTGRES_DB=kong" \
                   -e "POSTGRES_PASSWORD=kong" \
                   postgres:9.6

    第三步,初始kong数据

    docker run --rm \
         --network=kong-net \
         -e "KONG_DATABASE=postgres" \
         -e "KONG_PG_HOST=kong-database" \
         -e "KONG_PG_USER=kong" \
         -e "KONG_PG_PASSWORD=kong" \
         -e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" \
         kong:latest kong migrations bootstrap

    第四步,启动kong

    docker run -d --name kong \
         --network=kong-net \
         -e "KONG_DATABASE=postgres" \
         -e "KONG_PG_HOST=kong-database" \
         -e "KONG_PG_USER=kong" \
         -e "KONG_PG_PASSWORD=kong" \
         -e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" \
         -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
         -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \
         -e "KONG_PROXY_ERROR_LOG=/dev/stderr" \
         -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \
         -e "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" \
         -p 8000:8000 \
         -p 8443:8443 \
         -p 127.0.0.1:8001:8001 \
         -p 127.0.0.1:8444:8444 \
         kong:latest

    使用grpc-web插件

    完成了Kong的安装和初始化,简单的grpc服务也完成,现在就按照grpc-web官网上的方法来设置grpc-web插件。使用的是Kong admin命令来设置,最新版的kong-dashboard暂时没有升级到支持最新版的KONG,

    第一步,创建grpc服务

    curl -XPOST localhost:8001/services \
      --data name=grpc \
      --data protocol=grpc \
      --data host=localhost \
      --data port=6565

    第二步,创建http route

    curl -XPOST localhost:8001/services/grpc/routes \
      --data protocols=http \
      --data name=web-service \
      --data paths=/

    第三步,为此route设置grpc-web

    curl -XPOST localhost:8001/routes/web-service/plugins \
      --data name=grpc-web

    部署完成后,访问网址http://localhost:8000,返回400错误,原因unkonwn path /。

    问题分析

    出现这个错误,说明KONG已经接收到了此请求,但后端grpc服务没有收到请求,因此问题就出现在grpc-web插件上,于是打开grpc-web插件源码来进行分析。找到下面的代码片段:

     local dec, err = deco.new(
        kong_request_get_header("Content-Type"),
        kong_request_get_path(), conf.proto)
      if not dec then
        kong.log.err(err)
        return kong_response_exit(400, err)
      end

    此代码段的第五行返回400错误,因此问题可能发生在deco.new方法上,此方法返回nil导致错误发生,于是继续向上分析deco.new方法,代码如下:

    function deco.new(mimetype, path, protofile)
      local text_encoding = text_encoding_from_mime[mimetype]
      local framing = framing_form_mime[mimetype]
      local msg_encoding = msg_encodign_from_mime[mimetype]

      local input_type, output_type
      if msg_encoding ~= "proto" then
        if not protofile then
          return nil, "transcoding requests require a .proto file defining the service"
        end

        input_type, output_type = rpc_types(path, protofile)
        if not input_type then
          return nil, output_type
        end
      end

      return setmetatable({
        mimetype = mimetype,
        text_encoding = text_encoding,
        framing = framing,
        msg_encoding = msg_encoding,
        input_type = input_type,
        output_type = output_type,
      }, deco)
    end

    此处,有两处返回nil,第一处返回nil,应该不可能,因为我们设置了proto文件,所以怀疑应该是第二处的nil导致的,因此继续向上查找rpc_types方法,代码如下:

    local function rpc_types(path, protofile)
      if not protofile then
        return nil
      end

      local info = get_proto_info(protofile)
      local types = info[path]
      if not types then
        return nil, ("Unkown path %q"):format(path)
      end

      return types[1], types[2]
    end

    此代码中出错信息是Unknown path,因此基本断定是此处返回的错误信息,是由于info这个表中不能那个找到我们传入的path,继续向上分析如何产生这个info表的。也就是get_proto_info方法。

    local function get_proto_info(fname)
      local info = _proto_info[fname]
      if info then
        return info
      end

      local p = protoc.new()
      local parsed = p:parsefile(fname)

      info = {}

      for _, srvc in ipairs(parsed.service) do
        for _, mthd in ipairs(srvc.method) do
          info[("/%s.%s/%s"):format(parsed.package, srvc.name, mthd.name)] = {
            mthd.input_type,
            mthd.output_type,
          }
        end
      end

      _proto_info[fname] = info

      p:loadfile(fname)
      return info
    end

    从这个方法就能明白是如何产生info表的,首先从缓存中获取,如果能获取到直接返回,否则就解析proto文件生成info表,生成规则是proto里的package name,servicename和method name作为key,格式为info[("/%s.%s/%s"):format(parsed.package, srvc.name, mthd.name)] ,方法的input_type和output_type作为value。因此我么传入的/这个key肯定不能存在,所以会返回400错误。

    水落石出

    问题定位到了,解决方法就比较简单,在创建route时,path不能按照文档的实例中传入简单的/,而应该传入/package name.service name/method name这种格式。于是将创建route那步改成如下请求即可。

    curl -XPOST localhost:8001/services/grpc/routes \
      --data protocols=http \
      --data name=web-service \
      --data paths=/com.example.grpc.helloworld.HelloWorldService/sayHello

    测试

    curl  -H "Content-type: application/json" -XPOST -d '{"first_name": "david","last_name": "zhang"}'  http://localhost:8000/com.example.grpc.helloworld.HelloWorldService/sayHello 

    返回字符串:hello,david zhang!

    写在最后

    虽然花了很多时间最后解决了此问题,也能收获一点点的成就感,但总觉得此插件官网的文档太过简单,不知道是不是代码和文档没有配套更新导致的,理论上不应该犯这种低级错误,而且同一篇文章被引用了很多地方,都是同一种配置方法,估计都没有亲自验证就发布了。联系到前几天看到一篇公众号文章,发现里面有些问题,所以给作者留言了,我倒不是要较真,只是觉得一篇受众那么广的博客,还是需要对读者负责,不能误导读者。作者倒反馈很迅速,只是不太友好,哪里错误?说我没有亲自验证,呵呵,当我把错误列出来后,也没再回复我,悄悄的把错误改了。

    相关文章

      网友评论

          本文标题:Kong中使用grpc-web插件代理grpc服务时遇到的坑

          本文链接:https://www.haomeiwen.com/subject/jvvxrktx.html