一、为什么要做服务发现?

1.1 微服务里的“找路难题”

做微服务开发的人都懂,原来一个大项目拆成十几个甚至几十个小服务后,最头疼的就是“找服务”。比如用户下单这个操作,得先找订单服务、再找库存服务、最后找支付服务,要是每个服务的地址都写死在代码里,一旦某个服务换了服务器、改了端口,甚至加了新的实例,所有调用它的服务都得跟着改代码、重新上线,麻烦到爆炸。 服务发现就是解决这个问题的“指路牌”——它能自动记录每个服务的位置(IP、端口),需要调用的服务直接找服务发现工具要地址就行,不用自己记,也不用改代码。

二、Apollo是什么?为什么选它做服务发现?

2.1 Apollo的基础功能

很多人知道Apollo是配置中心,用来改参数不用重启服务,但其实它还自带服务发现的能力,而且不用额外装别的组件,省了不少维护成本。简单说,Apollo的服务发现核心就是两个功能:一是让每个服务上线时主动“报到”(注册),下线时主动“请假”(注销);二是让需要调用服务的人能从Apollo里拿到最新的服务地址列表。

2.2 选Apollo做服务发现的优缺点

先说说优点:第一,不用额外部署组件,只要你已经用Apollo做配置,直接开服务发现功能就行,省了装Eureka、Consul的功夫;第二,它的配置和服务发现是打通的,比如某个服务的地址改了,既能通过配置通知,也能通过服务发现更新;第三,稳定性够,毕竟很多大厂都在用,不会动不动掉链子。 再说说缺点:第一,它的服务发现功能是“轻量”的,没有专门的服务发现组件那么多高级功能,比如复杂的负载均衡策略、服务熔断,要是你需要这些,得自己加代码;第二,它的注册是“主动上报”,不是心跳检测,要是服务突然断网没注销,Apollo还会保留这个地址,容易出问题。

三、Apollo服务发现的核心原理

Apollo做服务发现的逻辑其实很简单,分三步: 第一步,每个服务启动时,会主动向Apollo的服务注册中心发请求,把自己的服务名、IP、端口、版本这些信息报上去,相当于“我上线了,记下来”; 第二步,Apollo会把所有注册的服务信息存起来,形成一个服务地址列表; 第三步,需要调用服务的一方,会通过Apollo的客户端接口,根据服务名拿到最新的地址列表,然后自己选一个地址调用就行。

四、Apollo服务发现的实践步骤(Java技术栈)

这里我们用Java作为技术栈,因为Apollo对Java的支持最完善,示例也最清晰。

4.1 准备工作

首先得有个已经部署好的Apollo服务端,不管是本地测试用的还是线上的都行。然后在你的Java项目里加Apollo的依赖,这个依赖同时包含了配置和服务发现的功能。

<!-- Apollo客户端依赖,同时支持配置和服务发现 -->
<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
    <artifactId>apollo-client</artifactId>
    <version>2.1.0</version> <!-- 用最新稳定版就行 -->
</dependency>

然后在项目的配置文件里加Apollo的基本配置,告诉项目要连哪个Apollo服务端:

# Apollo服务端地址,本地测试一般是http://localhost:8080
apollo.meta=http://localhost:8080
# 项目的AppId,每个项目唯一,要和Apollo里的配置对应
app.id=service-provider
# 环境,比如dev、test、prod
apollo.env=dev

4.2 服务提供者注册到Apollo

服务提供者就是被调用的一方,比如库存服务,它启动时要主动注册到Apollo。我们写一个简单的Spring Boot服务提供者示例:

import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

// 开启Apollo配置,同时会自动开启服务注册
@EnableApolloConfig
@SpringBootApplication
public class ServiceProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceProviderApplication.class, args);
    }

    // 提供一个测试接口,让消费者调用
    @RestController
    public class TestController {
        @GetMapping("/getStock")
        public String getStock() {
            return "当前库存:100";
        }
    }
}

这里要注意,@EnableApolloConfig注解会自动帮服务注册到Apollo,不用额外写注册代码,非常方便。服务启动后,Apollo的控制台就能看到这个服务的注册信息了。

4.3 服务消费者从Apollo获取服务地址

服务消费者就是调用别人服务的一方,比如订单服务,它需要从Apollo拿到库存服务的地址,然后调用库存服务的接口。我们写一个Spring Boot的消费者示例:

import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import com.ctrip.framework.apollo.core.dto.ServiceDTO;
import com.ctrip.framework.apollo.core.utils.ServiceLocator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

// 开启Apollo配置
@EnableApolloConfig
@SpringBootApplication
public class ServiceConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceConsumerApplication.class, args);
    }

    // 注入RestTemplate,用来发HTTP请求
    @RestController
    public class TestController {
        // 初始化RestTemplate
        private final RestTemplate restTemplate = new RestTemplate();

        @GetMapping("/callStockService")
        public String callStockService() {
            // 从Apollo获取库存服务的地址列表,服务名是service-provider(和提供者的AppId对应)
            List<ServiceDTO> serviceList = ServiceLocator.getServices("service-provider");
            
            // 这里简单选第一个地址,实际项目可以加负载均衡策略,比如轮询
            if (serviceList != null && !serviceList.isEmpty()) {
                ServiceDTO service = serviceList.get(0);
                // 拼接完整的接口地址:IP+端口+接口路径
                String url = String.format("http://%s:%d/getStock", service.getIp(), service.getPort());
                // 调用库存服务的接口
                String result = restTemplate.getForObject(url, String.class);
                return "调用库存服务结果:" + result;
            } else {
                return "未找到库存服务";
            }
        }
    }
}

这个示例里,ServiceLocator.getServices()就是Apollo提供的获取服务地址的方法,只要传入服务名,就能拿到所有注册的服务地址列表。

五、Apollo服务发现的注意事项

5.1 服务下线要主动注销

Apollo的服务注册是主动上报的,要是服务正常下线,一定要主动注销,不然Apollo还会保留这个地址,消费者调用就会出错。比如在Spring Boot项目里,可以加一个注销的逻辑:

import com.ctrip.framework.apollo.core.utils.ServiceLocator;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PreDestroy;

@Configuration
public class ServiceDestroyConfig {
    // 项目关闭前执行
    @PreDestroy
    public void destroy() {
        // 注销当前服务,服务名是自己的AppId
        ServiceLocator.unregisterService("service-provider");
    }
}

要是服务突然断网、宕机,没法主动注销,那可以用Apollo的“服务过期”功能,设置一个过期时间,超过时间没上报地址就自动注销,不过这个功能需要在Apollo服务端配置。

5.2 服务名要规范

服务名是Apollo区分不同服务的唯一标识,一定要规范,比如用“业务-功能”的格式,比如order-service、stock-service,不要随便起名字,不然消费者容易找错服务。

5.3 负载均衡要自己实现

Apollo的服务发现只提供地址列表,不提供负载均衡,比如有10个库存服务实例,消费者怎么选?是轮询、随机还是按权重?这些都要自己写代码实现,或者结合Spring Cloud的LoadBalancer组件来用。

5.4 版本控制要做好

要是同一个服务有多个版本,比如库存服务有v1和v2两个版本,注册的时候要把版本信息带上,消费者获取地址的时候可以根据版本选对应的实例,不然会出现调用版本不兼容的问题。

六、应用场景总结

Apollo的服务发现适合什么场景呢?首先是已经用Apollo做配置的项目,不用额外部署组件,成本低;其次是中小规模的微服务项目,服务数量不多,不需要太复杂的服务发现功能;还有就是快速迭代的项目,不用花时间搭建专门的服务发现组件,直接用Apollo就行。 要是你的项目服务数量很多,需要复杂的负载均衡、服务熔断、流量控制这些高级功能,那还是用专门的服务发现组件比较好。

七、实践总结

用Apollo做服务发现,最大的好处就是方便,不用额外部署组件,只要你用Apollo做配置,就能直接开服务发现功能,适合中小项目快速上手。它的原理也很简单,就是服务注册、地址存储、地址获取三步,只要注意服务主动注销、负载均衡自己实现这些细节,就能稳定运行。 当然,它也有自己的局限性,要是项目规模大了,还是要考虑专门的服务发现组件。不过对于大部分中小项目来说,Apollo的服务发现已经足够用了。