微服务架构中的GraphQL


背景

在最近一篇文章《 Exploring Tokens with Stellar》中我们提到了如何利用GraphQL拼接来打造解决方案。为了更好的理解,我们使用了一种简单优雅的方式进行了介绍,首先分享下我们解决方案的整体拓扑。
1.png

GraphQL环境下微服务日益增长的麻烦

在利用GraphQL构建编程模型时,我们发现在每一项功能当中引入离散服务能够使我们的团队快速工作并实现编程模型迭代,同时实现基于微服务与应用十二要素的一系列典型优势。另一方面,分离这些服务使得团队无法充分利用更多的诸如服务器批处理和缓存等GraphQL高级功能。同时,我们在并行状态下也试图简化客户端编程模型,以及Apollo如何在定义完善的端点上来初始化客户端,这为开发团队带来了其它的痛点。

GraphQL拼接如何解决我们的问题

为了解决我们的痛点,团队成员决定去看看其他人是如何使用GraphQL来处理微服务架构,然后很快就发现了模式拼接的方法。在我们的微服务架构里,每个微服务都有其独一无二的服务端点以及为入口控制器所配置的路由规则。这种方法要求前端开发团队定义多个Apollo客户机来支持各种服务端点,或者在入口处简单路由规则之外来配置复杂的路由规则以支持到各种微服务端点的路由。通过使用模式拼接,我们能够合并来自各种支持GraphQL接口的复合视图微服务的模式,从而可以简化我们的客户机编程模型。其结果将是一个可编程的单个GraphQL端点。另外,这使得我们的客户端编程模型能够真正使用跨越多个服务的GraphQL和批处理查询,而这在以前的体系结构中是不可能做到的。如果你想和更多微服务技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

GraphQL拼接的架构

GraphQL拼接是一种部署在现有拓扑上的附加运行服务(参见上面的拓扑)。我们的解决方案中在入口控制器和GraphQL端点之间部署了一个GraphQL代理进行路由流量。其核心功能由一个名为GraphQL - tools的社区Node.js模块提供,该模块由一组定义良好且需要实现的API接口组成。拼接的作用是从一组已经定义好的GraphQL端点中来导入和合并模式。此运行时服务负责将入站请求负载映射到对应合适的端点上,同时处理错误和保证安全性。

我们服务所使用的GraphQL拼接方法

在下面的例子中,包含了用于合并模式的整个资源,同时这些资源也使用各种GraphQL修改查询来初始化Apollo服务。请注意,拼接此时扮演了实际服务代理的角色,并且在模式合并完成后是静态不变的。因此,如果你正在管理的拼接的GraphQL模式是不稳定的,那么你将需要定期监控这些端点的变化。在我们的例子中,利用了Kubernetes的就绪性和活跃性探测来跟踪更改,并在发生更改时来触发服务的重新加载。
const {
    makeRemoteExecutableSchema,
    introspectSchema,
    mergeSchemas
} = require('graphql-tools');
const { createHttpLink } = require('apollo-link-http');
const { setContext } = require('apollo-link-context');
const { ApolloServer } = require('apollo-server-express');
const fetch = require('node-fetch');
const log4js = require('log4js');
const logger = log4js.getLogger('server.js');
logger.level = process.env.LOG_LEVEL || 'debug';
const express = require('express');

const app = express();
const _ = require('lodash');

const ACCOUNT_URI =
  process.env.BASE_URI || 'http://token-factory-account-service:4001'; // override for not in K8
const REGISTRATION_URI =
  process.env.BASE_URI || 'http://token-factory-registration-service:4000'; // override for not in K8
const STELLAR_NODE_URI = 'https://core-test.gly.sh/graphql'
let schema = false;

// graphql API metadata
const graphqlApis = [
    {
        uri: STELLAR_NODE_URI       //Token Factory APIs will override this default set
    },
    {
        uri: ACCOUNT_URI + '/account'
    },
    {
        uri: REGISTRATION_URI + '/registration'
    }
];

// authenticate for schema usage
const context = ({ req }) => {
    return { req };
};

// create executable schemas from remote GraphQL APIs
const createRemoteExecutableSchemas = async () => {
    let schemas = [];
    for (const api of graphqlApis) {
        const http = new createHttpLink({ uri: api.uri, fetch });

        const link = setContext((request, previousContext) => {
            return {
                headers: {
                    authorization: previousContext.graphqlContext
                        ? previousContext.graphqlContext.req.headers.authorization
                        : ''
                }
            };
        }).concat(http);

        const remoteSchema = await introspectSchema(link);
        const remoteExecutableSchema = makeRemoteExecutableSchema({
            schema: remoteSchema,
            link
        });
        schemas.push(remoteExecutableSchema);
    }
    return schemas;
};

const createNewSchema = async () => {
    const schemas = await createRemoteExecutableSchemas();
    if (!schemas) {
        return false;
    } else {
        return mergeSchemas({
            schemas
        });
    }
};

const port = 3001;
const path = '/token-factory';
app.path = path;

//Kub8 health check
app.get('/readiness', async function(req, res) {
    try {
        schema = await createNewSchema();
        if (schema) {
            const server = await new ApolloServer({ schema });
            server.applyMiddleware({ app, path });
            res.status(200).json({
                message: 'Graphql service is ready. All services are connected'
            });
        } else {
            res
                .status(500)
                .json({ err: 'Graphql service not ready. Waiting on services' });
        }
    } catch (error) {
        console.log('error', error);
        res.status(500).json({ err: 'Graphql service is unreachable' });
    }
});

app.get('/liveness', async function(req, res) {
    try {
        const tmpSchema = await createNewSchema();
        if (tmpSchema && _.differenceBy(tmpSchema, schema).length === 0) {
            res.status(200).json({
                message:
          'Graphql service is alive and no changes to schema have occurred'
            });
        } else {
            res.status(500).json({ err: 'Graphql schema has changed.' });
        }
    } catch (error) {
        console.log('error', error);
        res.status(500).json({ err: 'Graphql service is unreachable' });
    }
});

const startServer = async () => {
    app.listen({ port: port }, () => {
        logger.info(`App listening on <hostname>:${port}${app.path}!`);
    });

    try {
        schema = await createNewSchema();
        if (schema) {
            const server = new ApolloServer({
                schema,
                context: context
            });
            logger.info('schema merged', schema);
            server.applyMiddleware({ app, path });
        }
    } catch (error) {
        logger.info(
            'Failed to create schema during startup.  Defer to K8 probes',
            error
        );
    }
};

startServer();

远程第三方模式介绍

在上述程序脚本的22行可以看到这样的代码:
const STELLAR_NODE_URI = 'https://core-test.gly.sh/graphql'

GraphQL拼接最强大的功能之一是能够使用第三方的API接口并把它们与已有的GraphQL API接口合并在一起。Stellar开发人员社区十分活跃,并为开发人员提供了许多有用的帮助。我们可以依赖的是一个位于Stellar testnet的Postgres数据库之上支持GraphQL的API层,关于这点以及关于开发者生态系统的强大功能的案例可以在《Building Stellar Apps》这篇文章中找到。这个端点增加了我们的API,并为每个Stellar核心数据库公开了一组丰富的GraphQL查询集。这真是太棒了。

结论

站在以RESTAPI为中心的角度来看,GraphQL能够执行适合多个前端应用程序的细粒度查询,改变了游戏规则。通过使用GraphQL拼接,我们构建了健壮的API接口策略,既可用来消费使用,又具备了在未来添加额外微服务的扩展能力。

原文链接:GraphQL in a Micro Services Architecture(翻译:韦峻峰)

0 个评论

要回复文章请先登录注册