您当前的位置:首页 > 电脑百科 > 程序开发 > 框架

Dubbo + Nacos这么玩就失去高可用的能力了

时间:2023-09-08 13:06:46  来源:微信公众号  作者:不焦躁的程序员

我们常用的微服务框架是SpringCloud那一套,在服务远程调用和注册中心的选型上也有不少方案。在服务远程调用上常用的有:Feign、Dubbo等,在注册中心上常用的有:Nacos、Zookeeper、Consul、Eureka等。我们项目这两块的选型是这样的:RPC调用-Dubbo、注册中心和配置中心-Nacos。

一、故障开端

项目平稳运行了好几年,有一天发现Nacos集群的Server内存有点高,所以想升级下机器配置,然后重启。说干就干,立马在测试环境的3台Nacos-Server集群中,任意选了一台进行停机,暂且叫它Nacos-Server-1吧。接下来就是故障了开端了。

停机之后,测试环境立马有许多服务的接口调不通,等待许久,故障一直没恢复。所以又赶紧把Nacos-Server-1启动起来。要找找原因,否则无法在生产环境重启Nacos-Server。

我一直的观点是:出现疑难问题时,首先看异常信息,然后猜测原因,再通过实践去验证,最终可以通过源码再去证实。而不是一上来就看源码,那样比酱香配拿铁更伤头。

二、、异常信息

当Nacos-server-1停机时,首先在Nacos-Client(即某个微服务应用)看到异常,主要有2个:

  • nacos-client与nacos-server心跳异常
  • dubbo微服务调用异常

(1) nacos-client与nacos-server心跳异常:

2023-09-06 08:10:09|ERROR|com.alibaba.nacos.client.naming.NET.NamingProxy:reqApi|548|com.alibaba.nacos.naming.beat.sender|"request: /nacos/v1/ns/instance/beat failed, servers: [10.20.1.13:8848, 10.20.1.14:8848, 10.20.1.15:8848], code: 500, msg: JAVA.net.SocketTimeoutException: Read timed out"|""
2023-09-06 08:10:09|ERROR|com.alibaba.nacos.client.naming.beat.BeatReactor$BeatTask:run|198|com.alibaba.nacos.naming.beat.sender|"[CLIENT-BEAT] failed to send beat: {"port":0,"ip":"10.21.230.14","weight":1.0,"serviceName":"DEFAULT_GROUP@@consumers:com.cloud.usercenter.api.PartyCompanyMemberApi:1.0:","metadata":{"owner":"ehome-cloud-owner","init":"false","side":"consumer","Application.version":"1.0","methods":"queryGroupMemberCount,queryWithValid,query,queryOne,update,insert,queryCount,queryPage,delete,queryList","release":"2.7.8","dubbo":"2.0.2","pid":"6","check":"false","interface":"com.bm001.ehome.cloud.usercenter.api.PartyCompanyMemberApi","version":"1.0","qos.enable":"false","timeout":"20000","revision":"1.2.38-SNAPSHOT","retries":"0","path":"com.bm001.ehome.cloud.usercenter.api.PartyCompanyMemberApi","protocol":"consumer","metadata-type":"remote","application":"xxxx-cloud","sticky":"false","category":"consumers","timestamp":"1693917779436"},"scheduled":false,"period":5000,"stopped":false}, code: 500, msg: failed to req API:/nacos/v1/ns/instance/beat after all servers([10.20.1.13:8848, 10.20.1.14:8848, 10.20.1.15:8848])"|""
2023-09-06 08:10:10|ERROR|com.alibaba.nacos.client.naming.net.NamingProxy:callServer|613|com.alibaba.nacos.naming.beat.sender|"[NA] failed to request"|"com.alibaba.nacos.api.exception.NacosException: java.net.ConnectException: 拒绝连接 (Connection refused)
        at com.alibaba.nacos.client.naming.net.NamingProxy.callServer(NamingProxy.java:611)
        at com.alibaba.nacos.client.naming.net.NamingProxy.reqApi(NamingProxy.java:524)
        at com.alibaba.nacos.client.naming.net.NamingProxy.reqApi(NamingProxy.java:491)
        at com.alibaba.nacos.client.naming.net.NamingProxy.sendBeat(NamingProxy.java:426)
        at com.alibaba.nacos.client.naming.beat.BeatReactor$BeatTask.run(BeatReactor.java:167)
        
Caused by: java.io.IOException: Server returned HTTP response code: 502 for URL: http://10.20.1.14:8848/nacos/v1/ns/instance/beat?app=unknown&serviceName=DEFAULT_GROUP%40%40providers%3AChannelOrderExpressApi%3A1.0%3A&namespaceId=dev&port=20880&ip=10.20.0.200
  at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1914)
  at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1512)
  at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:480)
  at com.alibaba.nacos.common.http.client.response.JdkHttpClientResponse.getStatusCode(JdkHttpClientResponse.java:75)
  at com.alibaba.nacos.common.http.client.handler.AbstractResponseHandler.handle(AbstractResponseHandler.java:43)
"

(2) dubbo微服务调用异常:

2023-09-06 08:09:38|ERROR|runtimeExceptionHandler|135|http-nio-8080-exec-5|"发生系统异常"|"org.Apache.dubbo.rpc.RpcException: No provider available from registry 10.20.1.13:8848,10.20.1.14:8848,10.20.1.15:8848 for service ClueAuntMatchApi:1.0 on consumer 10.21.230.14 use dubbo version 2.7.8, please check status of providers(disabled, not registered or in blacklist).
        at org.apache.dubbo.registry.integration.RegistryDirectory.doList(RegistryDirectory.java:599)
        at org.apache.dubbo.rpc.cluster.directory.AbstractDirectory.list(AbstractDirectory.java:74)
        at org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker.list(AbstractClusterInvoker.java:292)
        at org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker.invoke(AbstractClusterInvoker.java:257)
        at org.apache.dubbo.rpc.cluster.interceptor.ClusterInterceptor.intercept(ClusterInterceptor.java:47)

三、根据异常进行猜测

熟悉Dubbo的朋友肯定知道这个错误please check status of providers(disabled, not registered or in blacklist).,基本上是代表:Provider下线了 或者 Consumer没找到Provider。

根据以往使用dubbo + zookeeper的经验,客户端应该会拉取注册中心的Provider的信息,然后本地缓存一份,即使注册中心挂了,应该也能调用到别的服务。不至于出现完全找不到服务提供者的信息。

当思考不出来时,只能靠异常去猜测原因了。根据以上2个异常开始猜测。

1.猜测1

由于nacos-server-1挂了,导致nacos-client与server的心跳异常,导致本地缓存的provider的元数据被清掉了。有了猜测,赶紧查看nacos-client的源代码,找到nacos-client 与 nacos-server 心跳的那一段:

图片

继续往下跟,可以看到这段核心代码:

public String reqApi(String api, Map<String, String> params, Map<String, String> body, List<String> servers,
            String method) throws NacosException {
        params.put(CommonParams.NAMESPACE_ID, getNamespaceId());
        if (CollectionUtils.isEmpty(servers) && StringUtils.isEmpty(nacosDomain)) {
            throw new NacosException(NacosException.INVALID_PARAM, "no server available");
        }
        NacosException exception = new NacosException();
        if (servers != null && !servers.isEmpty()) {
            Random random = new Random(System.currentTimeMillis());
            int index = random.nextInt(servers.size());
            
            for (int i = 0; i < servers.size(); i++) {
                String server = servers.get(index);
                try {
                    return callServer(api, params, body, server, method);
                } catch (NacosException e) {
                    exception = e;
                    if (NAMING_LOGGER.isDebugEnabled()) {
                        NAMING_LOGGER.debug("request {} failed.", server, e);
                    }
                }
                index = (index + 1) % servers.size();
            }
        }
        
        if (StringUtils.isNotBlank(nacosDomain)) {
            for (int i = 0; i < UtilAndComs.REQUEST_DOMAIN_RETRY_COUNT; i++) {
                try {
                    return callServer(api, params, body, nacosDomain, method);
                } catch (NacosException e) {
                    exception = e;
                    if (NAMING_LOGGER.isDebugEnabled()) {
                        NAMING_LOGGER.debug("request {} failed.", nacosDomain, e);
                    }
                }
            }
        }
        NAMING_LOGGER.error("request: {} failed, servers: {}, code: {}, msg: {}", api, servers, exception.getErrCode(),
                exception.getErrMsg());
        throw new NacosException(exception.getErrCode(),
                "failed to req API:" + api + " after all servers(" + servers + ") tried: " + exception.getMessage());
    }

注意上面的这段代码:

for (int i = 0; i < servers.size(); i++) {
    String server = servers.get(index);
    try {
        return callServer(api, params, body, server, method);
    } catch (NacosException e) {
        exception = e;
        if (NAMING_LOGGER.isDebugEnabled()) {
            NAMING_LOGGER.debug("request {} failed.", server, e);
        }
    }
    index = (index + 1) % servers.size();
}

通过以上这一段代码可以知道,nacos-client与nacos-server集群里的随机一台通信,感兴趣的朋友可以继续阅读源代码,跟到最后会发现,只要有一次心跳是正常的,那就认为心跳正常。因为我只停了一台nacos-server,但是与其他两台server依旧可以保持心跳,所以整个心跳过程虽然报错,但是仍然是正常的,所以这个猜测放弃了,继续猜测。

2.猜测2

既然dubbo与zookeeper是建立长连接进行socket通信,那dubbo与nacos-server可能也是建立了长连接进行socket通信,某个nacos-server挂了之后,可能因为nacos-server没有zookeeper的选主机制,所以不会自动切换到别的可用的nacos-server去调用。

或者是nacos-server集群选主问题,选主后没有及时通知到consumer,或者consumer与nacos本身通信机制有问题。总之就是因为某种机制,导致没有自动切换到可用的nacos-server上,导致获取不到provider元数据,自然就无法发起调用。

既然有了这个猜想,那就赶紧去证实:

继续翻看nacos源码,发现nacos提供了集群节点之间数据一致性保障,使用的是Raft协议(一致性的选主协议,最后在简单介绍),源代码如下:

既然有选主协议,那就看看为什么通信还是失败了呢?继续从nacos-server的异常信息入手,在nacos-server-1停机时,看到nacos-server的logs下多种异常信息:

在naming-raft.log里,如下异常信息:

java.lang.NullPointerException: null
  at com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore.signalDelete(RaftCore.java:275)
  at com.alibaba.nacos.naming.consistency.persistent.raft.RaftConsistencyServiceImpl.remove(RaftConsistencyServiceImpl.java:72)
  at com.alibaba.nacos.naming.consistency.DelegateConsistencyServiceImpl.remove(DelegateConsistencyServiceImpl.java:53)
  at com.alibaba.nacos.naming.core.ServiceManager.easyRemoveService(ServiceManager.java:434)
  at com.alibaba.nacos.naming.core.ServiceManager$EmptyServiceAutoClean.lambda$null$1(ServiceManager.java:902)
  at java.util.concurrent.ConcurrentHashMap.computeIfPresent(ConcurrentHashMap.java:1769)
  at com.alibaba.nacos.naming.core.ServiceManager$EmptyServiceAutoClean.lambda$null$2(ServiceManager.java:891)
  at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
  at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
  at java.util.concurrent.ConcurrentHashMap$EntrySpliterator.forEachRemaining(ConcurrentHashMap.java:3606)
  at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
  at java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:290)
  at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:731)
  at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
  at java.util.concurrent.ForkJoinPool.helpComplete(ForkJoinPool.java:1870)
  at java.util.concurrent.ForkJoinPool.externalHelpComplete(ForkJoinPool.java:2467)
  at java.util.concurrent.ForkJoinTask.externalAwaitDone(ForkJoinTask.java:324)
  at java.util.concurrent.ForkJoinTask.doInvoke(ForkJoinTask.java:405)
  at java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:734)
  at java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:159)
  at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(ForEachOps.java:173)
  at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
  at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:485)
  at com.alibaba.nacos.naming.core.ServiceManager$EmptyServiceAutoClean.lambda$run$3(ServiceManager.java:891)
  at java.util.concurrent.ConcurrentHashMap.forEach(ConcurrentHashMap.java:1597)
  at com.alibaba.nacos.naming.core.ServiceManager$EmptyServiceAutoClean.run(ServiceManager.java:881)
  at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
  at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
  at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)
  at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
  at java.lang.Thread.run(Thread.java:748)
2023-09-07 08:19:25,262 ERROR Raft remove failed.

naming-push.log里,如下异常信息:

java.lang.IllegalStateException: unable to find ackEntry for key: 10.21.140.23,43998,31247629183519634, ack json: {"type": "push-ack", "lastRefTime":"31247629183519634", "data":""}
  at com.alibaba.nacos.naming.push.PushService$Receiver.run(PushService.java:677)
  at java.lang.Thread.run(Thread.java:748)
2023-09-07 08:17:38,533 ERROR [NACOS-PUSH] error while receiving ack data

naming-distro.log里,如下异常信息:

2023-09-07 08:19:39,904 ERROR receive responsible key timestamp of com.alibaba.nacos.naming.iplist.ephemeral.dev-jzj##DEFAULT_GROUP@@providers:com.bm001.league.ordercenter.api.AdCluePoolApi:1.0: from 10.20.1.13:8848

将这些异常信息结合起来可以推断出,在nacos-server-1停机时,nacos-server集群只剩余2台机器,它们在利用Raft协议进行选主时,出现了异常。导致consumer没有找到主节点,无法建立正确的通信,所以consumer获取不到provider的元数据。

继续证实这个推断吧!

此时同时把nacos-server-1和nacos-server-2同时停机,只保留1台nacos-server时,微服务之间调用就正常了。因为单个节点时,选主正常,consumer很快与nacos-server建立了通信。此时再把3台全部启动后,也是一切正常。至此可以证实2台nacos-server确实存在选主问题。

至此问题解决,安心干活儿去了,哈哈!

3.Raft协议

简单讲下Raft协议,Raft协议主要用来满足微服务CAP理论中的CP,保障集群环境下的数据一致性。在Raft理论中,把每一个集群节点定义了三种状态,跟zookeeper的ZAB 协议类似:

Follower 追随者:集群所有节点一开始都是 Follower。

Candidate 候选者:当集群的某个节点开始发起投票选举 Leader 的时,先给自己投一票,这时就会从 Follower 变成 Candidate。

Leader 领导者:当集群的某个节点获得大多数节点(超过一半)的投票,那么就会变成 Leader。

四、总结

经过以上的过程,有3点注意:

  • nacos-client和nacos-server的心跳只是告诉服务器,我这个客户端的服务是正常的,同时nacos-server集群之间会异步同步服务信息。但是具体调用时依赖dubbo,dubbo在调用时,是单独的通道从nacos-server拉取provider的元数据。
  • nacos-server重启时,一定要选在深夜,避开正常流量时间。同时为了保障集群持续可用,集群节点数保持奇数,偶数时会出现选主问题,导致客户端与服务端无法正常通信,无法发起微服务调用。也就失去了nacos集群的能力了。
  • 出现疑难问题时,首先看异常信息,然后猜测原因,再通过实践去验证,最终可以通过源码再去证实。


Tags:Dubbo   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
图解Dubbo,Dubbo 服务治理详解
当前,分布式服务在互联网行业中得到了广泛应用。然而,分布式服务不仅仅是将单个应用程序分割成不同的模块,还涉及到模块之间的相互合作和协作。服务治理是分布式服务的一个关键...【详细内容】
2023-10-17  Search: Dubbo  点击:(218)  评论:(0)  加入收藏
深入理解java和dubbo的SPI机制
作者 | 京东云开发者-京东物流 龚航林原文链接:https://my.oschina.net/u/4090830/blog/101160111 SPI 简介1.1 SPI(Service Provider Interface)本质:将接口实现类的全限定名...【详细内容】
2023-10-11  Search: Dubbo  点击:(243)  评论:(0)  加入收藏
实例讲解SpringBoot集成Dubbo的步骤及过程
首先,让我们先了解一下Spring Boot和Dubbo。Spring Boot 是一个开源的 Java Web 框架,它可以帮助开发者快速创建独立的、生产级别的 Spring 应用程序。Spring Boot 提供了很多...【详细内容】
2023-09-26  Search: Dubbo  点击:(251)  评论:(0)  加入收藏
Dubbo + Nacos这么玩就失去高可用的能力了
我们常用的微服务框架是SpringCloud那一套,在服务远程调用和注册中心的选型上也有不少方案。在服务远程调用上常用的有:Feign、Dubbo等,在注册中心上常用的有:Nacos、Zookeeper...【详细内容】
2023-09-08  Search: Dubbo  点击:(264)  评论:(0)  加入收藏
如何将 Dubbo Filter 拦截器原理运用到日志拦截器中?
业务背景我们希望可以在使用日志拦截器时,定义属于自己的拦截器方法。实现的方式有很多种,我们分别来看一下。拓展阅读java 注解结合 spring aop 实现自动输出日志[1]java 注...【详细内容】
2023-08-06  Search: Dubbo  点击:(225)  评论:(0)  加入收藏
基于Dubbo解决亿级流量中缓存双写策略问题
1.引言在处理大规模流量和高并发读写请求的分布式系统中,缓存双写是一项关键任务。保证缓存的一致性和高可用性是挑战性的,特别是在面对亿级流量的场景下。本文将探讨亿级流量...【详细内容】
2023-05-16  Search: Dubbo  点击:(348)  评论:(0)  加入收藏
图解Dubbo,六种扩展机制详解
今天详细的分解一下Dubbo的扩展机制,实现快速入门,丰富个人简历,提高面试level,给自己增加一点谈资,秒变面试小达人,BAT不是梦。说真的,从零学习Dubbo,看这个系列足够了,共10篇,欢迎持...【详细内容】
2023-04-12  Search: Dubbo  点击:(158)  评论:(0)  加入收藏
Go 语言体系下的微服务框架选型: Dubbo-go
作 者 | 牛学蔚(蔚俊)本文介绍了Go 微服务体系发展与选型,过去一年Dubbo-go 社区的飞速发展以及对未来的展望。一、Go 微服务体系发展与选型随着微服务技术的快速发展,其在各...【详细内容】
2023-04-11  Search: Dubbo  点击:(273)  评论:(0)  加入收藏
如何用一个端口同时暴露 HTTP1/2、gRPC、Dubbo 协议?
本文我们将介绍 Apache Dubbo 灵活的多协议设计原则,基于这一设计,在 Dubbo 框架底层可灵活的选用 HTTP/2、HTTP/REST、TCP、gRPC、JsonRPC、Hessian2 等任一 RPC 通信协议,同...【详细内容】
2023-03-17  Search: Dubbo  点击:(84)  评论:(0)  加入收藏
阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别
大家好,我是三友~~今天来跟大家聊一聊Java、Spring、Dubbo三者SPI机制的原理和区别。其实我之前写过一篇类似的文章,但是这篇文章主要是剖析dubbo的SPI机制的源码,中间只是简单...【详细内容】
2023-03-14  Search: Dubbo  点击:(142)  评论:(0)  加入收藏
▌简易百科推荐
Web Components实践:如何搭建一个框架无关的AI组件库
一、让人又爱又恨的Web ComponentsWeb Components是一种用于构建可重用的Web元素的技术。它允许开发者创建自定义的HTML元素,这些元素可以在不同的Web应用程序中重复使用,并且...【详细内容】
2024-04-03  京东云开发者    Tags:Web Components   点击:(8)  评论:(0)  加入收藏
Kubernetes 集群 CPU 使用率只有 13% :这下大家该知道如何省钱了
作者 | THE STACK译者 | 刘雅梦策划 | Tina根据 CAST AI 对 4000 个 Kubernetes 集群的分析,Kubernetes 集群通常只使用 13% 的 CPU 和平均 20% 的内存,这表明存在严重的过度...【详细内容】
2024-03-08  InfoQ    Tags:Kubernetes   点击:(12)  评论:(0)  加入收藏
Spring Security:保障应用安全的利器
SpringSecurity作为一个功能强大的安全框架,为Java应用程序提供了全面的安全保障,包括认证、授权、防护和集成等方面。本文将介绍SpringSecurity在这些方面的特性和优势,以及它...【详细内容】
2024-02-27  风舞凋零叶    Tags:Spring Security   点击:(53)  评论:(0)  加入收藏
五大跨平台桌面应用开发框架:Electron、Tauri、Flutter等
一、什么是跨平台桌面应用开发框架跨平台桌面应用开发框架是一种工具或框架,它允许开发者使用一种统一的代码库或语言来创建能够在多个操作系统上运行的桌面应用程序。传统上...【详细内容】
2024-02-26  贝格前端工场    Tags:框架   点击:(47)  评论:(0)  加入收藏
Spring Security权限控制框架使用指南
在常用的后台管理系统中,通常都会有访问权限控制的需求,用于限制不同人员对于接口的访问能力,如果用户不具备指定的权限,则不能访问某些接口。本文将用 waynboot-mall 项目举例...【详细内容】
2024-02-19  程序员wayn  微信公众号  Tags:Spring   点击:(39)  评论:(0)  加入收藏
开发者的Kubernetes懒人指南
你可以将本文作为开发者快速了解 Kubernetes 的指南。从基础知识到更高级的主题,如 Helm Chart,以及所有这些如何影响你作为开发者。译自Kubernetes for Lazy Developers。作...【详细内容】
2024-02-01  云云众生s  微信公众号  Tags:Kubernetes   点击:(50)  评论:(0)  加入收藏
链世界:一种简单而有效的人类行为Agent模型强化学习框架
强化学习是一种机器学习的方法,它通过让智能体(Agent)与环境交互,从而学习如何选择最优的行动来最大化累积的奖励。强化学习在许多领域都有广泛的应用,例如游戏、机器人、自动驾...【详细内容】
2024-01-30  大噬元兽  微信公众号  Tags:框架   点击:(67)  评论:(0)  加入收藏
Spring实现Kafka重试Topic,真的太香了
概述Kafka的强大功能之一是每个分区都有一个Consumer的偏移值。该偏移值是消费者将读取的下一条消息的值。可以自动或手动增加该值。如果我们由于错误而无法处理消息并想重...【详细内容】
2024-01-26  HELLO程序员  微信公众号  Tags:Spring   点击:(84)  评论:(0)  加入收藏
SpringBoot如何实现缓存预热?
缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。那么问题来了,在 Spring Boot 项目启动之后,在什么时候?在哪里可以将数据加载到缓存系...【详细内容】
2024-01-19   Java中文社群  微信公众号  Tags:SpringBoot   点击:(86)  评论:(0)  加入收藏
花 15 分钟把 Express.js 搞明白,全栈没有那么难
Express 是老牌的 Node.js 框架,以简单和轻量著称,几行代码就可以启动一个 HTTP 服务器。市面上主流的 Node.js 框架,如 Egg.js、Nest.js 等都与 Express 息息相关。Express 框...【详细内容】
2024-01-16  程序员成功  微信公众号  Tags:Express.js   点击:(86)  评论:(0)  加入收藏
站内最新
站内热门
站内头条