一、为什么要做服务发现?
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的服务发现已经足够用了。
Comments