-
前言OOM 几乎是笔者工作中遇到的线上 bug 中最常见的,一旦平时正常的页面在线上出现页面崩溃或者服务无法调用,查看服务器日志后你很可能会看到“Caused by: java.lang.OutOfMlemoryError: Java heap space” 这样的提示,那么毫无疑问表示的是 Java 堆内存溢出了。其中又当属集合内存溢出最为常见。你是否有过把整个数据库表查出来的全字段结果直接赋值给一个 List 对象?是否把未经过过滤处理的数据赋值给 Set 对象进行去重操作?又或者是在高并发的场景下创建大量的集合对象未释放导致 JVM 无法自动回收?Java 堆内存溢出我的解决方案的核心思路有两个:一是从代码入手进行优化;二是从硬件层面对机器做合理配置。一、代码优化下面先说从代码入手怎么解决。1.1Stream 流自分页/** * 以下示例方法都在这个实现类里,包括类的继承和实现 */ @Service public class StudyServiceImpl extends ServiceImpl<StudyMapper, Study> implements StudyService{}在循环里使用 Stream 流的 skip()+limit() 来实现自分页,直至取出所有数据,不满足条件时终止循环 /** * 避免集合内存溢出方法(一) * @return */ private List<StudyVO> getList(){ ArrayList<StudyVO> resultList = new ArrayList<>(); //1、数据库取出源数据,注意只拿 id 字段,不至于溢出 List<String> idsList = this.list(new LambdaQueryWrapper<Study>() .select(Study::getId)).stream() .map(Study::getId) .collect(Collectors.toList()); //2、初始化循环 boolean loop = true; long number = 0; long perSize = 5000; while (loop){ //3、skip()+limit()组合,限制每次只取固定数量的 id List<String> ids = idsList.stream() .skip(number * perSize) .limit(perSize) .collect(Collectors.toList()); if (CollectionUtils.isNotEmpty(ids)){ //根据第3步的 id 去拿数据库的全字段数据,这样也不至于溢出,因为一次只是 5000 条 List<StudyVO> voList = this.listByIds(ids).stream() .map(e -> e.copyProperties(StudyVO.class)) .collect(Collectors.toList()); //addAll() 方法也比较关键,快速地批量添加元素,容量是比较大的 resultList.addAll(voList); } //4、判断是否跳出循环 number++; loop = ids.size() == perSize; } return resultList; }1.2数据库分页这里是用数据库语句查询符合条件的指定条数,循环查出所有数据,不满足条件就跳出循环 /** * 避免集合内存溢出方法(二) * @param param * @return */ private List<StudyVO> getList(String param){ ArrayList<StudyVO> resultList = new ArrayList<>(); //1、构造查询条件 String id = ""; //2、初始化循环 boolean loop = true; int perSize = 5000; while (loop){ //分页,固定每次循环都查 5000 条 Page<Study> studyPage = this.page(new Page<> (NumberUtils.INTEGER_ZERO, perSize), wrapperBuilder(param, id)); if (Objects.nonNull(studyPage)){ List<Study> studyList = studyPage.getRecords(); if (CollectionUtils.isNotEmpty(studyList)){ //3、每次截取固定数量的标识,数组下标减一 id = studyList.get(perSize - NumberUtils.INTEGER_ONE).getId(); //4、判断是否跳出循环 loop = studyList.size() == perSize; //添加进返回的 VO 集合中 resultList.addAll(studyList.stream() .map(e -> e.copyProperties(StudyVO.class)) .collect(Collectors.toList())); } else { loop = false; } } } return resultList; } /** * 条件构造 * @param param * @param id * @return */ private LambdaQueryWrapper<Study> wrapperBuilder(String param, String id){ LambdaQueryWrapper<Study> wrapper = new LambdaQueryWrapper<>(); //只查部分字段,按照 id 的降序排列,形成顺序 wrapper.select(Study::getUserAvatar) .eq(Study::getOpenId, param) .orderByAsc(Study::getId); if (StringUtils.isNotBlank(id)){ //这步很关键,只查比该 id 值大的数据 wrapper.gt(Study::getId, id); } return wrapper; }1.3其它思考以上从根本上还是解决不了内存里处理大量数据的问题,取出 50w 数据放内存的风险就很大了。以下是我的其它解决思路:从业务上拆解:明确什么情况下需要后端处理这么多数据?是否可以考虑在业务流程上进行拆解?或者用其它形式的页面交互代替?数据库设计:数据一般都来源于数据库,库/表设计的时候尽量将表与表之间解耦,表字段的颗粒度放细,即多表少字段,查询时只拿需要的字段;数据放在磁盘:比如放到 MQ 里存储,然后取出的时候注意按固定数量批次取,并且注意释放资源;异步批处理:如果业务对实时性要求不高的话,可以异步批量把数据添加到文件流里,再存入到 OSS 中,按需取用;定时任务处理:询问产品经理该功能或者实现是否是结果必须的?是否一定要同步处理?可以考虑在一个时间段内进行多次操作,缓解大数据量的问题;咨询大数据团队:寻求大数据部门团队的专业支持,对于处理海量数据他们是专业的,看能不能提供一些可参考的建议。二、硬件配置核心思路:加大服务器内存,合理分配服务器的堆内存,并设置好弹性伸缩规则,当触发告警时自动伸缩扩容,保证系统的可用性。2.1云服务器配置以下是阿里云 ECS 管理控制台的编辑页面,可以对 CPU 和内存进行配置。在 ECS 实例伸缩组创建完成后,即可以根据业务规模去创建一个自定义伸缩配置,在业务量大的时候会触发自动伸缩。阿里云 ECS 管理如果是部署在私有云服务器,需要对具体的 JVM 参数进行调优的话,可能还得请团队的资深大佬、或者运维团队的老师来帮忙处理。三、文章小结本篇文章主要是记录一次线上集合内存溢出问题的处理思路,在之后的文章中我会分享一些关于真实项目中处理高并发、缓存的使用、异步/解耦等内容,敬请期待。那么今天的分享到这里就结束了,如有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区交流!转载自https://www.cnblogs.com/CodeBlogMan/p/18022444
-
在日常开发中,Date工具类使用频率相对较高,大家通常都会这样写: public static Date getData(String date) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(date); } public static Date getDataByFormat(String date, String format) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat(format); return sdf.parse(date); } 这很简单啊,有什么争议吗? 你应该听过“时区”这个名词,大家也都知道,相同时刻不同时区的时间是不一样的。 因此在使用时间时,一定要给出时区信息。 public static void getDataByZone(String param, String format) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat(format); // 默认时区解析时间表示 Date date = sdf.parse(param); System.out.println(date + ":" + date.getTime()); // 东京时区解析时间表示 sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo")); Date newYorkDate = sdf.parse(param); System.out.println(newYorkDate + ":" + newYorkDate.getTime()); } public static void main(String[] args) throws ParseException { getDataByZone("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss"); } 对于当前的上海时区和纽约时区,转化为 UTC 时间戳是不同的时间。 对于同一个本地时间的表示,不同时区的人解析得到的 UTC 时间一定是不同的,反过来不同的本地时间可能对应同一个 UTC。 格式化后出现的时间错乱。 public static void getDataByZoneFormat(String param, String format) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat(format); Date date = sdf.parse(param); // 默认时区格式化输出 System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date)); // 东京时区格式化输出 TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo")); System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date)); } public static void main(String[] args) throws ParseException { getDataByZoneFormat("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss"); } 我当前时区的 Offset(时差)是 +8 小时,对于 +9 小时的纽约,整整差了1个小时,北京早上 10 点对应早上东京 11 点。 看看Java 8是如何解决时区问题的: Java 8 推出了新的时间日期类 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime 和 DateTimeFormatter,处理时区问题更简单清晰。 public static void getDataByZoneFormat8(String param, String format) throws ParseException { ZoneId zone = ZoneId.of("Asia/Shanghai"); ZoneId tokyoZone = ZoneId.of("Asia/Tokyo"); ZoneId timeZone = ZoneOffset.ofHours(2); // 格式化器 DateTimeFormatter dtf = DateTimeFormatter.ofPattern(format); ZonedDateTime date = ZonedDateTime.of(LocalDateTime.parse(param, dtf), zone); // withZone设置时区 DateTimeFormatter dtfz = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z"); System.out.println(dtfz.withZone(zone).format(date)); System.out.println(dtfz.withZone(tokyoZone).format(date)); System.out.println(dtfz.withZone(timeZone).format(date)); } public static void main(String[] args) throws ParseException { getDataByZoneFormat8("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss"); } Asia/Shanghai对应+8,对应2023-11-10 10:00:00; Asia/Tokyo对应+9,对应2023-11-10 11:00:00; timeZone 是+2,所以对应2023-11-10 04:00:00; 在处理带时区的国际化时间问题,推荐使用jdk8的日期时间类: 通过ZoneId,定义时区; 使用ZonedDateTime保存时间; 通过withZone对DateTimeFormatter设置时区; 进行时间格式化得到本地时间; 思路比较清晰,不容易出错。 在与前端联调时,报了个错,java.lang.NumberFormatException: multiple points,起初我以为是时间格式传的不对,仔细一看,不对啊。 百度一下,才知道是高并发情况下SimpleDateFormat有线程安全的问题。 下面通过模拟高并发,把这个问题复现一下: public static void getDataByThread(String param, String format) throws InterruptedException { ExecutorService threadPool = Executors.newFixedThreadPool(5); SimpleDateFormat sdf = new SimpleDateFormat(format); // 模拟并发环境,开启5个并发线程 for (int i = 0; i < 5; i++) { threadPool.execute(() -> { for (int j = 0; j < 2; j++) { try { System.out.println(sdf.parse(param)); } catch (ParseException e) { System.out.println(e); } } }); } threadPool.shutdown(); threadPool.awaitTermination(1, TimeUnit.HOURS); } 果不其然,报错。还将2023年转换成2220年,我勒个乖乖。 在时间工具类里,时间格式化,我都是这样弄的啊,没问题啊,为啥这个不行?原来是因为共用了同一个SimpleDateFormat,在工具类里,一个线程一个SimpleDateFormat,当然没问题啦! 可以通过TreadLocal 局部变量,解决SimpleDateFormat的线程安全问题。 public static void getDataByThreadLocal(String time, String format) throws InterruptedException { ExecutorService threadPool = Executors.newFixedThreadPool(5); ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat(format); } }; // 模拟并发环境,开启5个并发线程 for (int i = 0; i < 5; i++) { threadPool.execute(() -> { for (int j = 0; j < 2; j++) { try { System.out.println(sdf.get().parse(time)); } catch (ParseException e) { System.out.println(e); } } }); } threadPool.shutdown(); threadPool.awaitTermination(1, TimeUnit.HOURS); } 看一下SimpleDateFormat.parse的源码: public class SimpleDateFormat extends DateFormat { @Override public Date parse(String text, ParsePosition pos){ CalendarBuilder calb = new CalendarBuilder(); Date parsedDate; try { parsedDate = calb.establish(calendar).getTime(); // If the year value is ambiguous, // then the two-digit year == the default start year if (ambiguousYear[0]) { if (parsedDate.before(defaultCenturyStart)) { parsedDate = calb.addYear(100).establish(calendar).getTime(); } } } } } class CalendarBuilder { Calendar establish(Calendar cal) { boolean weekDate = isSet(WEEK_YEAR) && field[WEEK_YEAR] > field[YEAR]; if (weekDate && !cal.isWeekDateSupported()) { // Use YEAR instead if (!isSet(YEAR)) { set(YEAR, field[MAX_FIELD + WEEK_YEAR]); } weekDate = false; } cal.clear(); // Set the fields from the min stamp to the max stamp so that // the field resolution works in the Calendar. for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) { for (int index = 0; index <= maxFieldIndex; index++) { if (field[index] == stamp) { cal.set(index, field[MAX_FIELD + index]); break; } } } ... } } 先new CalendarBuilder(); 通过parsedDate = calb.establish(calendar).getTime();解析时间; establish方法内先cal.clear(),再重新构建cal,整个操作没有加锁; 上面几步就会导致在高并发场景下,线程1正在操作一个Calendar,此时线程2又来了。线程1还没来得及处理 Calendar 就被线程2清空了。 因此,通过编写Date工具类,一个线程一个SimpleDateFormat,还是有一定道理的。 ———————————————— 原文链接:https://blog.csdn.net/guorui_java/article/details/135611142
-
为什么需要熔断 微服务集群中,每个应用基本都会依赖一定数量的外部服务。有可能随时都会遇到网络连接缓慢,超时,依赖服务过载,服务不可用的情况,在高并发场景下如果此时调用方不做任何处理,继续持续请求故障服务的话很容易引起整个微服务集群雪崩。比如高并发场景的用户订单服务,一般需要依赖一下服务: 商品服务 账户服务 库存服务 假如此时 账户服务 过载,订单服务持续请求账户服务只能被动的等待账户服务报错或者请求超时,进而导致订单请求被大量堆积,这些无效请求依然会占用系统资源:cpu,内存,数据连接...导致订单服务整体不可用。即使账户服务恢复了订单服务也无法自我恢复。 这时如果有一个主动保护机制应对这种场景的话订单服务至少可以保证自身的运行状态,等待账户服务恢复时订单服务也同步自我恢复,这种自我保护机制在服务治理中叫熔断机制。 熔断 熔断是调用方自我保护的机制(客观上也能保护被调用方),熔断对象是外部服务。 降级 降级是被调用方(服务提供者)的防止因自身资源不足导致过载的自我保护机制,降级对象是自身。 熔断这一词来源时我们日常生活电路里面的熔断器,当负载过高时(电流过大)保险丝会自行熔断防止电路被烧坏,很多技术都是来自生活场景的提炼。 工作原理 熔断器一般具有三个状态: 关闭:默认状态,请求能被到达目标服务,同时统计在窗口时间成功和失败次数,如果达到错误率阈值将会进入断开状态。 断开:此状态下将会直接返回错误,如果有 fallback 配置则直接调用 fallback 方法。 半断开:进行断开状态会维护一个超市时间,到达超时时间开始进入 半断开 状态,尝试允许一部门请求正常通过并统计成功数量,如果请求正常则认为此时目标服务已恢复进入 关闭 状态,否则进入 断开 状态。半断开 状态存在的目的在于实现了自我修复,同时防止正在恢复的服务再次被大量打垮。 使用较多的熔断组件: hystrix circuit breaker(不再维护) hystrix-go resilience4j(推荐) sentinel(推荐) 什么是自适应熔断 基于上面提到的熔断器原理,项目中我们要使用好熔断器通常需要准备以下参数: 错误比例阈值:达到该阈值进入 断开 状态。 断开状态超时时间:超时后进入 半断开 状态。 半断开状态允许请求数量。 窗口时间大小。 实际上可选的配置参数还有非常非常多,参考 https://resilience4j.readme.io/docs/circuitbreaker 对于经验不够丰富的开发人员而言,这些参数设置多少合适心里其实并没有底。 那么有没有一种自适应的熔断算法能让我们不关注参数,只要简单配置就能满足大部分场景? 其实是有的,google sre提供了一种自适应熔断算法来计算丢弃请求的概率: 算法参数: requests:窗口时间内的请求总数 accepts:正常请求数量 K:敏感度,K 越小越容易丢请求,一般推荐 1.5-2 之间 算法解释: 正常情况下 requests=accepts,所以概率是 0。 随着正常请求数量减少,当达到 requests == K* accepts 继续请求时,概率 P 会逐渐比 0 大开始按照概率逐渐丢弃一些请求,如果故障严重则丢包会越来越多,假如窗口时间内 accepts==0 则完全熔断。 当应用逐渐恢复正常时,accepts、requests 同时都在增加,但是 K*accepts 会比 requests 增加的更快,所以概率很快就会归 0,关闭熔断。 代码实现 接下来思考一个熔断器如何实现。 初步思路是: 无论什么熔断器都得依靠指标统计来转换状态,而统计指标一般要求是最近的一段时间内的数据(太久的数据没有参考意义也浪费空间),所以通常采用一个 滑动时间窗口 数据结构 来存储统计数据。同时熔断器的状态也需要依靠指标统计来实现可观测性,我们实现任何系统第一步需要考虑就是可观测性,不然系统就是一个黑盒。 外部服务请求结果各式各样,所以需要提供一个自定义的判断方法,判断请求是否成功。可能是 http.code 、rpc.code、body.code,熔断器需要实时收集此数据。 当外部服务被熔断时使用者往往需要自定义快速失败的逻辑,考虑提供自定义的 fallback() 功能。 下面来逐步分析 go-zero 的源码实现: core/breaker/breaker.go 熔断器接口定义 兵马未动,粮草先行,明确了需求后就可以开始规划定义接口了,接口是我们编码思维抽象的第一步也是最重要的一步。 核心定义包含两种类型的方法: Allow():需要手动回调请求结果至熔断器,相当于手动挡。 DoXXX():自动回调请求结果至熔断器,相当于自动挡,实际上 DoXXX() 类型方法最后都是调用 DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error // 自定义判定执行结果 Acceptable func(err error) bool // 手动回调 Promise interface { // Accept tells the Breaker that the call is successful. // 请求成功 Accept() // Reject tells the Breaker that the call is failed. // 请求失败 Reject(reason string) } Breaker interface { // 熔断器名称 Name() string // 熔断方法,执行请求时必须手动上报执行结果 // 适用于简单无需自定义快速失败,无需自定义判定请求结果的场景 // 相当于手动挡。。。 Allow() (Promise, error) // 熔断方法,自动上报执行结果 // 自动挡。。。 Do(req func() error) error // 熔断方法 // acceptable - 支持自定义判定执行结果 DoWithAcceptable(req func() error, acceptable Acceptable) error // 熔断方法 // fallback - 支持自定义快速失败 DoWithFallback(req func() error, fallback func(err error) error) error // 熔断方法 // fallback - 支持自定义快速失败 // acceptable - 支持自定义判定执行结果 DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error } 熔断器实现 circuitBreaker 继承 throttle,实际上这里相当于静态代理,代理模式可以在不改变原有对象的基础上增强功能,后面我们会看到 go-zero 这样做的原因是为了收集熔断器错误数据,也就是为了实现可观测性。 熔断器实现采用静态代理模式,看起来稍微有点绕脑。 // 熔断器结构体 circuitBreaker struct { name string // 实际上 circuitBreaker熔断功能都代理给 throttle来实现 throttle }// 熔断器接口 throttle interface { // 熔断方法 allow() (Promise, error) // 熔断方法 // DoXXX()方法最终都会该方法 doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error } func (cb *circuitBreaker) Allow() (Promise, error) { return cb.throttle.allow() } func (cb *circuitBreaker) Do(req func() error) error { return cb.throttle.doReq(req, nil, defaultAcceptable) } func (cb *circuitBreaker) DoWithAcceptable(req func() error, acceptable Acceptable) error { return cb.throttle.doReq(req, nil, acceptable) } func (cb *circuitBreaker) DoWithFallback(req func() error, fallback func(err error) error) error { return cb.throttle.doReq(req, fallback, defaultAcceptable) } func (cb *circuitBreaker) DoWithFallbackAcceptable(req func() error, fallback func(err error) error, acceptable Acceptable) error { return cb.throttle.doReq(req, fallback, acceptable) } throttle 接口实现类: loggedThrottle 增加了为了收集错误日志的滚动窗口,目的是为了收集当请求失败时的错误日志。 // 带日志功能的熔断器 type loggedThrottle struct { // 名称 name string // 代理对象 internalThrottle // 滚动窗口,滚动收集数据,相当于环形数组 errWin *errorWindow } // 熔断方法 func (lt loggedThrottle) allow() (Promise, error) { promise, err := lt.internalThrottle.allow() return promiseWithReason{ promise: promise, errWin: lt.errWin, }, lt.logError(err) } // 熔断方法 func (lt loggedThrottle) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error { return lt.logError(lt.internalThrottle.doReq(req, fallback, func(err error) bool { accept := acceptable(err) if !accept { lt.errWin.add(err.Error()) } return accept })) } func (lt loggedThrottle) logError(err error) error { if err == ErrServiceUnavailable { // if circuit open, not possible to have empty error window stat.Report(fmt.Sprintf( "proc(%s/%d), callee: %s, breaker is open and requests dropped\nlast errors:\n%s", proc.ProcessName(), proc.Pid(), lt.name, lt.errWin)) } return err } 错误日志收集 errorWindow errorWindow 是一个环形数组,新数据不断滚动覆盖最旧的数据,通过取余实现。 // 滚动窗口 type errorWindow struct { reasons [numHistoryReasons]string index int count int lock sync.Mutex } // 添加数据 func (ew *errorWindow) add(reason string) { ew.lock.Lock() // 添加错误日志 ew.reasons[ew.index] = fmt.Sprintf("%s %s", timex.Time().Format(timeFormat), reason) // 更新index,为下一次写入数据做准备 // 这里用的取模实现了滚动功能 ew.index = (ew.index + 1) % numHistoryReasons // 统计数量 ew.count = mathx.MinInt(ew.count+1, numHistoryReasons) ew.lock.Unlock() } // 格式化错误日志 func (ew *errorWindow) String() string { var reasons []string ew.lock.Lock() // reverse order for i := ew.index - 1; i >= ew.index-ew.count; i-- { reasons = append(reasons, ew.reasons[(i+numHistoryReasons)%numHistoryReasons]) } ew.lock.Unlock() return strings.Join(reasons, "\n") } 看到这里我们还没看到实际的熔断器实现,实际上真正的熔断操作被代理给了 internalThrottle 对象。 internalThrottle interface { allow() (internalPromise, error) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error } internalThrottle 接口实现 googleBreaker 结构体定义 type googleBreaker struct { // 敏感度,go-zero中默认值为1.5 k float64 // 滑动窗口,用于记录最近一段时间内的请求总数,成功总数 stat *collection.RollingWindow // 概率生成器 // 随机产生0.0-1.0之间的双精度浮点数 proba *mathx.Proba } 可以看到熔断器属性其实非常简单,数据统计采用的是滑动时间窗口来实现。 RollingWindow 滑动窗口 滑动窗口属于比较通用的数据结构,常用于最近一段时间内的行为数据统计。 它的实现非常有意思,尤其是如何模拟窗口滑动过程。 先来看滑动窗口的结构体定义: RollingWindow struct { // 互斥锁 lock sync.RWMutex // 滑动窗口数量 size int // 窗口,数据容器 win *window // 滑动窗口单元时间间隔 interval time.Duration // 游标,用于定位当前应该写入哪个bucket offset int // 汇总数据时,是否忽略当前正在写入桶的数据 // 某些场景下因为当前正在写入的桶数据并没有经过完整的窗口时间间隔 // 可能导致当前桶的统计并不准确 ignoreCurrent bool // 最后写入桶的时间 // 用于计算下一次写入数据间隔最后一次写入数据的之间 // 经过了多少个时间间隔 lastTime time.Duration } window 是数据的实际存储位置,其实就是一个数组,提供向指定 offset 添加数据与清除操作。数组里面按照 internal 时间间隔分隔成多个 bucket。 // 时间窗口 type window struct { // 桶 // 一个桶标识一个时间间隔 buckets []*Bucket // 窗口大小 size int } // 添加数据 // offset - 游标,定位写入bucket位置 // v - 行为数据 func (w *window) add(offset int, v float64) { w.buckets[offset%w.size].add(v) } // 汇总数据 // fn - 自定义的bucket统计函数 func (w *window) reduce(start, count int, fn func(b *Bucket)) { for i := 0; i < count; i++ { fn(w.buckets[(start+i)%w.size]) } } // 清理特定bucket func (w *window) resetBucket(offset int) { w.buckets[offset%w.size].reset() } // 桶 type Bucket struct { // 当前桶内值之和 Sum float64 //当前桶的add总次数 Count int64 } // 向桶添加数据 func (b *Bucket) add(v float64) { // 求和 b.Sum += v // 次数+1 b.Count++ } // 桶数据清零 func (b *Bucket) reset() { b.Sum = 0 b.Count = 0 } window 添加数据: 计算当前时间距离上次添加时间经过了多少个 时间间隔,实际上就是过期了几个 bucket。 清理过期桶的数据 更新 offset,更新 offset 的过程实际上就是在模拟窗口滑动 添加数据 // 添加数据 func (rw *RollingWindow) Add(v float64) { rw.lock.Lock() defer rw.lock.Unlock() // 获取当前写入的下标 rw.updateOffset() // 添加数据 rw.win.add(rw.offset, v) } // 计算当前距离最后写入数据经过多少个单元时间间隔 // 实际上指的就是经过多少个桶 func (rw *RollingWindow) span() int { offset := int(timex.Since(rw.lastTime) / rw.interval) if 0 <= offset && offset < rw.size { return offset } // 大于时间窗口时 返回窗口大小即可 return rw.size } // 更新当前时间的offset // 实现窗口滑动 func (rw *RollingWindow) updateOffset() { // 经过span个桶的时间 span := rw.span() // 还在同一单元时间内不需要更新 if span <= 0 { return } offset := rw.offset // 既然经过了span个桶的时间没有写入数据 // 那么这些桶内的数据就不应该继续保留了,属于过期数据清空即可 // 可以看到这里全部用的 % 取余操作,可以实现按照下标周期性写入 // 如果超出下标了那就从头开始写,确保新数据一定能够正常写入 // 类似循环数组的效果 for i := 0; i < span; i++ { rw.win.resetBucket((offset + i + 1) % rw.size) } // 更新offset rw.offset = (offset + span) % rw.size now := timex.Now() // 更新操作时间 // 这里很有意思 rw.lastTime = now - (now-rw.lastTime)%rw.interval } window 统计数据: // 归纳汇总数据 func (rw *RollingWindow) Reduce(fn func(b *Bucket)) { rw.lock.RLock() defer rw.lock.RUnlock() var diff int span := rw.span() // 当前时间截止前,未过期桶的数量 if span == 0 && rw.ignoreCurrent { diff = rw.size - 1 } else { diff = rw.size - span } if diff > 0 { // rw.offset - rw.offset+span之间的桶数据是过期的不应该计入统计 offset := (rw.offset + span + 1) % rw.size // 汇总数据 rw.win.reduce(offset, diff, fn) } } googleBreaker 判断是否应该熔断 收集滑动窗口内的统计数据 计算熔断概率 // 按照最近一段时间的请求数据计算是否熔断 func (b *googleBreaker) accept() error { // 获取最近一段时间的统计数据 accepts, total := b.history() // 计算动态熔断概率 weightedAccepts := b.k * float64(accepts) // https://landing.google.com/sre/sre-book/chapters/handling-overload/#eq2101 dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1)) // 概率为0,通过 if dropRatio <= 0 { return nil } // 随机产生0.0-1.0之间的随机数与上面计算出来的熔断概率相比较 // 如果随机数比熔断概率小则进行熔断 if b.proba.TrueOnProba(dropRatio) { return ErrServiceUnavailable } return nil } googleBreaker 熔断逻辑实现 熔断器对外暴露两种类型的方法 简单场景直接判断对象是否被熔断,执行请求后必须需手动上报执行结果至熔断器。 func (b *googleBreaker) allow() (internalPromise, error) 复杂场景下支持自定义快速失败,自定义判定请求是否成功的熔断方法,自动上报执行结果至熔断器。 func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error Acceptable 参数目的是自定义判断请求是否成功。 Acceptable func(err error) bool // 熔断方法 // 返回一个promise异步回调对象,可由开发者自行决定是否上报结果到熔断器 func (b *googleBreaker) allow() (internalPromise, error) { if err := b.accept(); err != nil { return nil, err } return googlePromise{ b: b, }, nil } // 熔断方法 // req - 熔断对象方法 // fallback - 自定义快速失败函数,可对熔断产生的err进行包装后返回 // acceptable - 对本次未熔断时执行请求的结果进行自定义的判定,比如可以针对http.code,rpc.code,body.code func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error { // 判定是否熔断 if err := b.accept(); err != nil { // 熔断中,如果有自定义的fallback则执行 if fallback != nil { return fallback(err) } return err } // 如果执行req()过程发生了panic,依然判定本次执行失败上报至熔断器 defer func() { if e := recover(); e != nil { b.markFailure() panic(e) } }() // 执行请求 err := req() // 判定请求成功 if acceptable(err) { b.markSuccess() } else { b.markFailure() } return err } // 上报成功 func (b *googleBreaker) markSuccess() { b.stat.Add(1) } // 上报失败 func (b *googleBreaker) markFailure() { b.stat.Add(0) } // 统计数据 func (b *googleBreaker) history() (accepts, total int64) { b.stat.Reduce(func(b *collection.Bucket) { accepts += int64(b.Sum) total += b.Count }) return } 原文链接: https://mp.weixin.qq.com/s?__biz=Mzg2ODU1MTI0OA==&mid=2247484672&idx=1&sn=43067f7af6b3c6233c15a14cb2ed7505&utm_source=tuicool&utm_medium=referral ———————————————— 原文链接:https://blog.csdn.net/m0_67645544/article/details/123738412
-
什么是微服务的熔断机制 在2017年2月1日,GitLab公司的运维人员就出现过这样的事故。当时运维人员在进行数据库维护时,通过执行rm -rf命令,删除了约300GB生产环境数据。由于数据备份失效,导致整个网站宕机数十个小时。 自2017年5月12日起,全球范围内爆发基于Windows网络共享协议进行攻击传播的蠕虫恶意代码,这是不法分子通过改造之前泄露的NSA黑客武器库中“永恒之蓝”攻击程序发起的网络攻击事件,用户只要开机上网就可被攻击。短短几个小时内,包括英国、俄罗斯、整个欧洲及国内多个高校校内网、大型企业内网和政府机构专网遭到了攻击,被勒索支付高额赎金才能解密恢复文件,对重要数据造成严重的损失。 可见信息系统的安全是一个无法忽视的问题。无论是个人还是组织,即便是最简单的系统,都需要考虑安全防护的措施。服务的熔断机制就是一种对网站进行防护的措施。 服务熔断的定义 对于“熔断”一词,大家应该都不会陌生,在中国股市,就曾经在2016年1月1日至2016年1月8日期间,实施过两次熔断机制。在微服务架构中,服务熔断本质上与股市的熔断机制并无差异,其出发点都是为了更好地控制风险。 服务熔断也称服务隔离或过载保护。在微服务应用中,服务存在一定的依赖关系,形成一定的依赖链,如果某个目标服务调用慢或者有大量超时,造成服务不可用,间接导致其他的依赖服务不可用,最严重的可能会阻塞整条依赖链,最终导致业务系统崩溃(又称雪崩效应)。此时,对该服务的调用执行熔断,对于后续请求,不再继续调用该目标服务,而是直接返回,从而可以快速释放资源。等到目标服务情况好转后,则可恢复其调用。 断路器 断路器(Circuit Breaker)本身是一个电子硬件产品,是电器中一个重要组成部分。断路器可用来分配电能,不用频繁地启动异步电动机,对电源线路及电动机等实行保护,当它们发生严重的过载或者短路及欠压等故障时能自动切断电路,其功能相当于熔断器式开关与过欠热继电器等的组合。 在微服务架构中,也存在所谓断路器或者实现断路器模式的软件构件。将受保护的服务封装在一个可以监控故障的断路器对象中,当故障达到一定门限时,断路器将跳闸,所有后继调用将不会发往受保护的服务而由断路器对象之间返回错误。对于需要更长时间解决的故障问题,由于不断重试没有太大意义了,所以就可以使用断路器模式。 路器模式 Michael Nygard在他编著的书Release lt!中推广了断路器模式。断路器模式致力于防止应用程序反复尝试执行可能失败的操作。允许它继续而不用等待故障被修复,或者在确定故障持续的时候浪费CPU周期。断路器模式还使应用程序能够检测故障是否已解决。如果问题似乎已经解决,应用程序可以尝试调用该操作。 断路器模式的目的不同于重试模式。重试模式使应用程序可以在预期成功的情况下重试操作。 断路器模式阻止应用程序执行可能失败的操作。应用程序可以通过使用重试模式及断路器模式来进行组合。然而,如果断路器指示故障不是瞬态的,则重试逻辑应该对断路器返回异常,并放弃重试尝试。 断路器充当可能失败的操作的代理。代理应监视最近发生的故障的数量,并使用此信息来决定是允许操作继续,还是立即返回异常。 代理可以作为一个状态机来实现,其状态模拟一个电气断路器的功能。 ·关闭(Closed):来自应用程序的请求被路由到操作。代理维护最近失败次数的计数,如果对操作的调用不成功,代理将增加此计数。如果在给定的时间段内最近的失败次数超过了指定的阈值,则代理被置于打开状态。此时代理启动一个超时定时器,当这个定时器超时时,代理被置于半开状态。超时定时器的目的是让系统有时间来解决导致失败的问题,然后再允许应用程序尝试再次执行操作。 ·打开(Open):来自应用程序的请求立即失败,并将异常返回给应用程序。 ·半打开 Half-Open 来自应用程序的有限数量的请求被允许通过并调用操作。如果这些请求成功,则认为先前引起故障的故障已被修复,断路器切换到关闭状态(故障计数器被重置)。如果有任何请求失败,断路器会认为故障仍然存在,因此它将恢复到打开状态,并重新启动超时定时器,以使系统有一段时间从故障中恢复。半开状态有助于防止恢复服务突然被请求淹没。当服务恢复时,它可能能够支持有限的请求量,直到恢复完成,但在进行恢复时,大量工作可能导致服务超时或再次失败。 图15-1展示的是 Microsoft Azure关于断路器状态的设计图。在该图中,关闭状态使用的故障计数器是基于时间的。它会定期自动重置。如果遇到偶尔的故障,这有助于防止断路器进入打开状态。只有在指定的时间间隔内发生指定次数的故障时,才会使断路器跳闸到断路状态的故障阈值。 半打开状态使用的计数器记录调用操作的成功尝试次数。在指定次数的连续操作调用成功后,断路器恢复到关闭状态。如果调用失败,断路器将立即进入打开状态,下一次进入半打开状态时,成功计数器将被重置。 系统恢复的方式可以通过恢复或重新启动故障组件或者修复网络连接来进行外部处理。 Spring Cloud Hystrix可以用来处理依赖隔离,实现熔断机制。其主要的类有HystrixCommand和HystrixObservableCommand等。 熔断的意义 在软件系统中,不可能百分之百保证不存在故障。为了保障整体系统的可用性和容错性,需要将服务实例部署在云或分布式系统环境中。 所以,我们必须承认服务一定是会出现故障的,只有清醒地认识到服务系统的本质,才能更好地去设计系统,来不断提高服务的可用性和容错性。 微服务的故障不可避免,这些故障可能是瞬时的,如慢的网络连接、超时,资源过度使用而暂时不可用;也可能是不容易预见的突发事件的情况下需要更长时间来纠正的故障。针对分布式服务的容错,通常的做法有两种。 ·重试机制,对于预期的短暂故障问题,通过重试模式是可以解决的。 ·断路器模式。 断路器模式所带来的好处 断路器模式提供了稳定性,同时系统从故障中恢复并最大限度地减少对性能的影响。通过快速拒绝可能失败的操作的请求,而不是等待操作超时或永不返回,可以帮助维持系统的响应时间。如果断路器每次改变状态都会产生一个事件,这个信息可以用来监测断路器所保护的系统部分的健康状况,或者在断路器跳到断路状态时提醒管理员。 断路器模式通常是可定制的,可以根据可能的故障类型进行调整。例如,可以自定义定时器的超时。您可以先将断路器置于“打开”状态几秒,然后如果故障仍未解决,则将超时增加到几分钟。 断路器模式的功能 一般来说,断路器具备如下功能。 1.异常处理 通过断路器调用操作的应用程序必须能够处理在操作不可用时可能被抛出的异常,该类异常的处理方式都是应用程序特有的。例如,应用程序会暂时降级其功能,调用备选操作尝试相同的任务或获取相同的数据,或者将异常通知给用户让其稍后重试。 一个请求可能由于各种原因失败,其中有一些可能表明故障严重类型高于其他故障。例如,一个请求可能由于需要几分钟才能恢复的远程服务崩溃而失败,也可能由于服务暂时超载造成的超时而失败。断路器有可能可以检查发生的异常类型,并根据这些异常类型来调整策略。例如,促使切换到打开状态的服务超时异常个数要远多于服务完全不可用导致的故障个数。 2.日志记录 一个断路器应记录所有失败的请求(如果可能的话记录所有请求),以使管理员能够监视它封装下受保护操作的运行状态。 3.可恢复 应该把断路器配置成与受保护操作最匹配的恢复模式。例如,如果设定断路器为打开状态的时间需要很长,即使底层操作故障已经解决,它还会返回错误。如果打开状态切换到半打开态过快,底层操作故障还没解决,它就会再次调用受保护操作。 4.测试失败的操作 在打开状态下,断路器可能不用计时器来确定何时切换到半打开状态,而是通过周期性地查验远程服务或资源以确定它是否已经再次可用。这个检查可能采用上次失败的操作的形式,也可以使用由远程服务提供的专门用于测试服务健康状况的特殊操作。 5.手动复位 在一个系统中,如果一个失败的操作的恢复时间差异很大,则提供一个手动复位选项,以使管理员能够强行关闭断路器及重置故障计数器。同样,如果受保护操作暂时不可用,管理员可以强制断路器进入打开状态并重新启动超时定时器。 6.并发 同—断路器可以被应用程序的大量并发实例访问。断路器实现不应阻塞并发请求或对每一请求增加额外开销。 7.加速断路 有时失败响应对于断路器实现来说包含足够的信息用于判定是否应当立即跳闸,并保持最小时间量的跳闸状态。例如,从过载共享资源的错误响应中可能指示了“不推荐立即重试”,那么应用程序应当隔几分钟之后再进行重试,而不应该立即重试。 如果一个请求的服务对于特定Web服务器不可用,可以返回HTTP协议定义的“HTTP 503Service Unavailable”响应。该响应可以包含额外的信息,如预期延迟持续时间。 8.重试失败请求 在打开状态下,断路器可以不仅仅是快速地简单返回失败,而是可以将每个请求的详细信息记录日志,并在远程资源或服务重新可用时安排重试。 ———————————————— 原文链接:https://blog.csdn.net/m0_63437643/article/details/123149700
-
熔断 微服务架构中,如果需要保障可用性,其中一个方式就是 熔断。熔断在微服务架构里面是指 当微服务本身出现问题的时候,它会拒绝新的请求,直到微服务恢复。通过熔断机制可以给服务端恢复的时间,比如 CPU 使用率已经超出负载了,此时服务端触发了熔断,那么新来的请求就会被拒绝,因此,服务端的 CPU 使用率就会在一段时间内降到100%以内。 判定服务的健康状态 判断微服务是否出现了问题,需要根据自己的业务来选择一些指标来代表这个服务器的健康程度,一般可以使用 响应时间、错误率。不管选择什么指标,都要考虑两个因素: 一是阈值如何选择;二是超过阈值之后,要不要持续一段时间才触发熔断。 假设 把响应时间作为指标,如果业务对响应时间的要求是在 1s 以内,那么你的阈值就可以设定在 1s,或者稍高一点,留点容错的余地也可以,原则上阈值应该明显超过正常响应时间。比如经过一段时间的观测之后,发现这个服务的 99 线是 1s,那么你可以考虑将熔断阈值设定为 1.2s。 如果响应时间一旦超过了阈值,也不能立刻就熔断,而是 要求响应时间超过一段时间之后才触发熔断。这主要是出于两个考虑,一个是响应时间可能是偶发性地突然增长;另外一个则是防止抖动。这个“一段时间”很大程度上就依赖个人经验了,如果时间过短,可能会频繁触发熔断,然后又恢复,再熔断,再恢复…… 反过来,如果时间过长,就可能会导致需要触发熔断的时候却迟迟没有触发。你可以根据经验来设定一个值,比如说三十秒或者一分钟。 总结:为了保障微服务的可用性,在核心服务里面可以接入熔断,针对不同的服务,可以设计不同的微服务熔断策略。比如最简单的熔断策略就是根据响应时间来判定,当响应时间超过阈值一段时间之后就会触发熔断;一般会根据业务情况来选择这个阈值,如果产品方要求响应时间是1s,那么可以把阈值设定在1.2s。如果响应时间超过1.2s,并且持续三十秒,就会触发熔断。在触发熔断的情况下,新请求会被拒绝,而已有的请求还是会被继续处理,直到服务恢复正常。 还可以根据缓存策略设计熔断方案,比如某一个接口得并发很高,对缓存的依赖度非常严重,所以当检测到缓存不可用的时候(比如说 Redis 崩溃了),那么就会触发熔断,因为此时如果不熔断的话,高并发的请求会因为 Redis 崩溃而全部落到 MySQL 上,导致压垮 MySQL。 在触发熔断之后,可以额外开启一个线程(例如 Goroutine)持续不断地 ping Redis的服务是否正常,如果 Redis 恢复了,那么就退出熔断状态,新来的请求就不会被拒绝了。 这种方案类似于处理缓存雪崩的问题,参考:《缓存雪崩、缓存击穿、穿透穿透具体指哪些问题?》 服务恢复正常 如果我们判断一个服务响应时间过长,进入了熔断状态,那么一段时间(比如十分钟)过后,已接收的请求已经被处理完了,也就是服务已经恢复正常了,那么就需要退出熔断状态,继续接收新请求。因此在触发熔断之后,就要考虑检测服务是否已经恢复正常。 如果本身熔断是高并发引起的,那么在一分钟后并发依旧很高,这时候你一旦直接恢复正常,然后高并发的流量打过来,服务是不是又会触发熔断? 这就会出现上面说的“抖动”的情况。 所谓 抖动:就是服务频繁地在正常-熔断两个状态之间切换。引起抖动的原因是多样的,比如说前面提到的一旦超过阈值就进入熔断状态;再比如“一分钟后就认为服务已经恢复正常,继续处理新请求”就容易引发抖动问题。 要解决这个抖动问题,就需要在恢复之后控制住流量。比如说按照 10%、20%、30%……逐步递增,而不是立刻恢复 100% 的流量(需要负载均衡来配合)。在这种逐步放开流量的措施下,依旧有请求因为熔断不会被处理,那么有没有更好的处理方式? 其实,可以让客户端来控制这个流量。就是当服务端触发熔断之后,客户端就直接不再请求这个节点了,而是换一个节点。等到恢复了之后,客户端再逐步对这个节点放开流量。整体思路是利用负载均衡来控制流量,如果一个服务端节点触发了熔断,那么客户端在做负载均衡的时候就可以将这个节点挪出可用列表,后续请求会发给别的节点。在经过一段时间之后,客户端可以尝试发请求给该节点。如果该节点正确处理了,那客户端就可以加大流量,否则客户端就要再一次等待一段时间。 综合运用负载均衡和熔断的方案,重点在于客户端控制流量,并根据服务端节点的状况来操作可用节点列表。参考:《微服务的注册发现和微服务架构下的负载均衡》 万一所有可用节点都触发熔断了,应该怎么办? 如果因为某些原因数据库出问题,导致某个服务所有的节点都触发了熔断,那么客户端就完全没有可用节点了。针对这个问题,熔断解决不了,负载均衡也解决不了,只能通过监控告警之后人手工介入处理了。 整体流程: 服务端在触发熔断的时候,会返回一个代表熔断的错误。 客户端在收到这个错误之后,就会把这个服务端节点暂时挪出可用节点列表。后续所有的新请求都不会再打到这个触发了熔断的服务端节点上了。 客户端在等待一段时间后,逐步放开流量 如果服务端正常处理了新来的请求,那么客户端就加大流量。 如果服务端再次返回了熔断响应,那么客户端就会再一次将这个节点挪出可用列表。 如此循环,直到服务端完全恢复正常,客户端也正常发送请求到该服务端节点。 降级 降级就是在服务资源不够用的时候,停用一部分边缘业务,这部分被停用的边缘业务可以被理解为“全部熔断了”。 比如在双十一之类的大促高峰,电商平台可能会关闭一些服务(比如退款服务)用来保证订单业务尽可能不受影响(当然营销策略部分不在我们的讨论范围),这就是降级的典型应用,不过它是一种手动的跨服务降级。这种降级的好处有两方面:一方面是腾出了服务器资源,可以给订单服务或者支付服务;另外一方面是减少了对公共组件的压力,比如说减少了对数据库的写入压力。 关于服务降级,主要关心的也是两个方面,其一、如何判定一个服务要不要降级(如何判定服务健康);其二、降级之后怎么恢复,也是要考虑抖动的问题。熔断是彻底不提供服务,而降级则是尽量提供不分服务。 所以在一些场景下,既可以用熔断,也可以用降级。比如说在响应时间超过阈值之后,可以考虑选择熔断,完全不提供服务;也可以考虑降级,提供一部分服务。 原则上来说,是应该优先考虑使用降级的,然而有些服务是无法降级的,尤其是写服务。例如 你从前端接收数据,然后写到数据库,这种场景是无法降级的。另外,如果你希望系统负载尽快降低,那么熔断要优于降级。 如何降级?基本上可以分成两大类:跨服务降级 和 本服务提供有损服务。 跨服务降级 当服务资源不够的时候可以暂停某些服务,将腾出来的资源给其他更加重要、更加核心的服务使用。(上面提到的大促期间暂停退款服务就是跨服务降级的例子)。这种策略的要点是,必须要确定一个服务比另外一个服务更有业务价值,或者更加重要。 跨服务降级的措施常见的做法有三个: 整个服务停掉,例如前面提到的停掉退款服务。 停掉服务的部分节点,例如十个节点,停掉其中五个节点,这五个节点被挪作他用。 停止访问某些资源。例如日志中心压力很大的时候,发信号给某些不重要的服务,让它们停止上传日志,只在本地保存日志。 跨服务降级可以在大部分合并部署的服务里面使用,一般的原则就是 B、C端合并部署降级 B 端;付费服务和非付费服务降级非付费服务。当然也可以根据自己的业务价值,将这些部署在同一个节点上的服务分成三六九等。而后在触发降级的时候从不重要的服务开始降级,将资源调配给重要服务。 自身服务提供有损服务 例如各大 App 的首页都会有降级的策略。在没有触发降级的时候,App 首页是针对你个人用户画像的个性化推荐。而在触发了降级之后,则可能是使用榜单数据,或者使用一个运营提前配置好的静态页面。这种策略的要点是你得知道你的服务调用者能够接受什么程度的有损。 针对服务本身的一些常见的降级思路: 返回默认值,这算是最简单的一种状况。 禁用可观测性组件,正常来说在业务里面都充斥了各种各样的埋点。这些埋点本身其实是会带来消耗的,所以在性能达到瓶颈的时候,就可以考虑停用,或者降低采样率。 同步转异步,即正常情况下,服务收到请求之后会立刻处理。但是在降级的情况下,服务在收到请求之后只会返回一个代表“已接收”的响应。后续服务会异步地开启线程来处理,或者依赖于定时任务来处理。 简化流程,如果你处理一个请求需要很多步骤,后续如果有一些步骤不关键的话,可以考虑不执行,或者异步执行。例如在内容生产平台,一般新内容要被推送到推荐系统里面。那么在降级的情况下你可以不推,而后可以考虑异步推送过去,也可以考虑等系统恢复之后再推送过去。 需要注意的是,在任何的故障处理里面,都要考虑恢复策略会不会引起抖动问题。 也可以考虑使用降级来保护 缓存-数据库 结构,一般来说,基本上都是先从缓存里面读数据,如果缓存里面没有数据,就从数据库中读取。那么在触发降级的情况下,可以考虑只从缓存里面读取,如果缓存里面没有数据,那么就直接返回,而不会再去数据库里读取。 这样可以保证在缓存里面有数据的那部分请求可以得到正常处理,也就是提供了有损服务。如果完全不考虑从数据库里取数据,那么你的性能瓶颈就完全取决于缓存,那么服务能够撑住的 QPS 会非常高。但是,如果缓存不命中的时候要去数据库取数据,那么服务的性能会衰退得非常快,即极少数缓存未命中的请求会占据大部分的系统资源。 具体案例分析 如果你的某个服务是同时提供了读服务和写服务,并且读服务明显比写服务更加重要,那么这时候你就可以考虑降级写服务。 假如说现在有一个针对商家的服务(比如 某团外卖),商家调用这些 API 来录入一些数据,比如他们门店的基本信息,上传一些门店图片等。同时还有一个针对 C 端普通用户的服务,这个服务就是把商家录入的数据展示在商家门店的首页上。所以你可以看到在这个场景下, 读服务 QPS 更高,也更加重要。那么如果这两个服务是一起部署的,在需要降级的时候,就可以考虑将针对商家的写服务停掉,将资源都腾出来给针对 C 端用户的读服务。从资源占用的角度分析,虽然整体来说写服务 QPS 占比很低,但是对于数据库来说,一次写请求对性能的压力要远比一次读请求大。所以暂停了写服务之后,数据库的负载能够减轻不少。 具体解决方案1( 读写服务中 降级写服务):接入一个跨服务的降级策略,当发现读服务的响应时间超过了阈值的时候,或者响应时间开始显著上升的时候,就将针对 B 端商家用户的服务临时停掉,腾出来的资源都给 C 端用户使用。对于 B 端用户来说,他们这个阶段是没有办法修改已经录入的数据的,但是这并不是一个特别大的问题。当 C 端接口的响应时间恢复正常之后,会自动恢复 B 端商家接口,商家又可以修改或者录入数据了。 在内容生产平台,作者生产内容,C 端用户查看生产的内容。那么在资源不足的情况下可以考虑停掉内容生产端的服务,只保留 C 端用户查看内容的功能。如果你的用户分成普通用户和 VIP 用户,那么你也可以考虑停掉给普通用户的服务。甚至,如果一个服务既提供给普通用户,也提供给 VIP 用户,你可以考虑将普通用户请求拒绝掉,只服务 VIP 用户。毕竟,VIP用户花钱了! 判断一个服务的业务价值最简单的方法就是判定什么业务带来了多少价值,又或者根据公司的主要营收来源确定服务的业务价值,越是能赚钱的就越重要。 具体解决方案2(快慢路径中 降级慢路径):在查询商品数据中,先查询缓存,如果缓存有数据,那么就直接返回。如果缓存没有,那么就需要去数据库查询。如果此时系统的并发非常高,那么就采取降级策略,将请求标记为降级请求。降级请求只会查询缓存,而不会查询数据库。如果缓存没有,那就直接返回错误。这样能够有效防止因为少部分请求缓存未命中而占据大量系统资源,导致系统吞吐量下降和响应时间显著升高。 这种思路其实可以在很多微服务里面应用。如果一个服务可以分成快路径和慢路径两种逻辑,那么在降级之前就可以先走快路径,再走慢路径。而触发了降级之后,就只允许走快路径。上面例子中,从缓存里加载数据就是快路径,从数据库里面加载数据就是慢路径。 慢路径还可以是发起服务调用或者复杂计算。比如说一个服务快路径是直接查询缓存,而慢路径可能是发起很多微服务调用,拿到所有响应之后一起计算,算出来一个结果并缓存起来。那么在降级的时候,可以有效提高吞吐量。不过这种吞吐量是有损的,毕竟部分请求如果没有在缓存中找到数据,那么就会直接返回失败响应。 ———————————————— 原文链接:https://blog.csdn.net/rxbook/article/details/134510266
-
服务熔断在介绍熔断机制之前,我们需要了解微服务的雪崩效应。在微服务架构中,微服务是完成一个单一的业务功能,这样做的好处是可以做到解耦,每个微服务可以独立演进。但是,一个应用可能会有多个微服务组成,微服务之间的数据交互通过远程过程调用完成。这就带来一个问题,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。熔断机制是应对雪崩效应的一种微服务链路保护机制。我们在各种场景下都会接触到熔断这两个字。高压电路中,如果某个地方的电压过高,熔断器就会熔断,对电路进行保护。股票交易中,如果股票指数过高,也会采用熔断机制,暂停股票的交易。同样,在微服务架构中,熔断机制也是起着类似的作用。当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。在dubbo中也可利用nio超时+失败次数做熔断。dubbo可以通过扩展Filter的方式引入Hystrix,具体代码如下:1234567891011121314151617181920package com.netease.hystrix.dubbo.rpc.filter;import com.alibaba.dubbo.common.Constants;import com.alibaba.dubbo.common.extension.Activate;import com.alibaba.dubbo.rpc.Filter;import com.alibaba.dubbo.rpc.Invocation;import com.alibaba.dubbo.rpc.Invoker;import com.alibaba.dubbo.rpc.Result;import com.alibaba.dubbo.rpc.RpcException;@Activate(group = Constants.CONSUMER)public class HystrixFilter implements Filter { @Override public Result invoke(Invoker invoker, Invocation invocation) throws RpcException { DubboHystrixCommand command = new DubboHystrixCommand(invoker, invocation); return command.execute(); }}DubboHystrixCommand12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061package com.netease.hystrix.dubbo.rpc.filter;import org.apache.log4j.Logger;import com.alibaba.dubbo.common.URL;import com.alibaba.dubbo.rpc.Invocation;import com.alibaba.dubbo.rpc.Invoker;import com.alibaba.dubbo.rpc.Result;import com.netflix.hystrix.HystrixCommand;import com.netflix.hystrix.HystrixCommandGroupKey;import com.netflix.hystrix.HystrixCommandKey;import com.netflix.hystrix.HystrixCommandProperties;import com.netflix.hystrix.HystrixThreadPoolProperties;public class DubboHystrixCommand extends HystrixCommand { private static Logger logger = Logger.getLogger(DubboHystrixCommand.class); private static final int DEFAULT_THREADPOOL_CORE_SIZE = 30; private Invoker invoker; private Invocation invocation; public DubboHystrixCommand(Invoker invoker,Invocation invocation){ super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(invoker.getInterface().getName())) .andCommandKey(HystrixCommandKey.Factory.asKey(String.format("%s_%d", invocation.getMethodName(), invocation.getArguments() == null ? 0 : invocation.getArguments().length))) .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() .withCircuitBreakerRequestVolumeThreshold(20)//10秒钟内至少19此请求失败,熔断器才发挥起作用 .withCircuitBreakerSleepWindowInMilliseconds(30000)//熔断器中断请求30秒后会进入半打开状态,放部分流量过去重试 .withCircuitBreakerErrorThresholdPercentage(50)//错误率达到50开启熔断保护 .withExecutionTimeoutEnabled(false))//使用dubbo的超时,禁用这里的超时 .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(getThreadPoolCoreSize(invoker.getUrl()))));//线程池为30 this.invoker=invoker; this.invocation=invocation; } /** * 获取线程池大小 * * @param url * @return */ private static int getThreadPoolCoreSize(URL url) { if (url != null) { int size = url.getParameter("ThreadPoolCoreSize", DEFAULT_THREADPOOL_CORE_SIZE); if (logger.isDebugEnabled()) { logger.debug("ThreadPoolCoreSize:" + size); } return size; } return DEFAULT_THREADPOOL_CORE_SIZE; } @Override protected Result run() throws Exception { return invoker.invoke(invocation); }}线程池大小可以通过dubbo参数进行控制,当前其他的参数也可以通过类似的方式进行配置代码添加好后在,resource添加加载文本|-resources|-META-INF|-dubbo|-com.alibaba.dubbo.rpc.Filter (纯文本文件,内容为:hystrix=com.netease.hystrix.dubbo.rpc.filter.HystrixFilter由于Filter定义为自动激活的,所以启动代码所有消费者都被隔离起来啦!熔段解决如下几个问题: 当所依赖的对象不稳定时,能够起到快速失败的目的 快速失败后,能够根据一定的算法动态试探所依赖对象是否恢复服务降级降级是指自己的待遇下降了,从RPC调用环节来讲,就是去访问一个本地的伪装者而不是真实的服务。 当双11活动时,把无关交易的服务统统降级,如查看蚂蚁深林,查看历史订单,商品历史评论,只显示最后100条等等。区别相同点:目的很一致,都是从可用性可靠性着想,为防止系统的整体缓慢甚至崩溃,采用的技术手段;最终表现类似,对于两者来说,最终让用户体验到的是某些功能暂时不可达或不可用;粒度一般都是服务级别,当然,业界也有不少更细粒度的做法,比如做到数据持久层(允许查询,不允许增删改);自治性要求很高,熔断模式一般都是服务基于策略的自动触发,降级虽说可人工干预,但在微服务架构下,完全靠人显然不可能,开关预置、配置中心都是必要手段;区别:触发原因不太一样,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;管理目标的层次不太一样,熔断其实是一个框架级的处理,每个微服务都需要(无层级之分),而降级一般需要对业务有层级之分(比如降级一般是从最外围服务开始)实现方式不太一样;服务降级具有代码侵入性(由控制器完成/或自动降级),熔断一般称为自我熔断。服务限流在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。一般开发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率);其他还有如限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。限流算法常见的限流算法有:令牌桶、漏桶。计数器也可以进行粗暴限流实现。漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量.应用级限流对于一个应用系统来说一定会有极限并发/请求数,即总有一个TPS/QPS阀值,如果超了阀值则系统就会不响应用户请求或响应的非常慢,因此我们最好进行过载保护,防止大量请求涌入击垮系统。如果你使用过Tomcat,其Connector其中一种配置有如下几个参数:acceptCount:如果Tomcat的线程都忙于响应,新来的连接会进入队列排队,如果超出排队大小,则拒绝连接;maxConnections:瞬时最大连接数,超出的会排队等待;maxThreads:Tomcat能启动用来处理请求的最大线程数,如果请求处理量一直远远大于最大线程数则可能会僵死。详细的配置请参考官方文档。另外如MySQL(如max_connections)、Redis(如tcp-backlog)都会有类似的限制连接数的配置。池化技术如果有的资源是稀缺资源(如数据库连接、线程),而且可能有多个系统都会去使用它,那么需要限制应用;可以使用池化技术来限制总资源数:连接池、线程池。比如分配给每个应用的数据库连接是100,那么本应用最多可以使用100个资源,超出了可以等待或者抛异常。限流某个接口的总并发/请求数如果接口可能会有突发访问情况,但又担心访问量太大造成崩溃,如抢购业务;这个时候就需要限制这个接口的总并发/请求数总请求数了;因为粒度比较细,可以为每个接口都设置相应的阀值。可以使用Java中的AtomicLong进行限流:12345678try {if(atomic.incrementAndGet() > 限流数) {//拒绝请求 }//处理请求} finally { atomic.decrementAndGet();}分布式限流分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使使用redis+lua或者nginx+lua技术进行实现,通过这两种技术可以实现的高并发和高性能。首先我们来使用redis+lua实现时间窗内某个接口的请求数限流,实现了该功能后可以改造为限流总并发/请求数和限制总资源数。Lua本身就是一种编程语言,也可以使用它实现复杂的令牌桶或漏桶算法。有人会纠结如果应用并发量非常大那么redis或者nginx是不是能抗得住;不过这个问题要从多方面考虑:你的流量是不是真的有这么大,是不是可以通过一致性哈希将分布式限流进行分片,是不是可以当并发量太大降级为应用级限流;对策非常多,可以根据实际情况调节;像在京东使用Redis+Lua来限流抢购流量,一般流量是没有问题的。对于分布式限流目前遇到的场景是业务上的限流,而不是流量入口的限流;流量入口限流应该在接入层完成,而接入层笔者一般使用Nginx。基于Redis功能的实现限流简陋的设计思路:假设一个用户(用IP判断)每分钟访问某一个服务接口的次数不能超过10次,那么我们可以在Redis中创建一个键,并此时我们就设置键的过期时间为60秒,每一个用户对此服务接口的访问就把键值加1,在60秒内当键值增加到10的时候,就禁止访问服务接口。在某种场景中添加访问时间间隔还是很有必要的。基于令牌桶算法的实现令牌桶算法最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。Java实现我们可以使用Guava 的 RateLimiter 来实现基于令牌桶的流控,RateLimiter 令牌桶算法是单桶实现。RateLimiter 对简单的令牌桶算法做了一些工程上的优化,具体的实现是 SmoothBursty。需要注意的是,RateLimiter 的另一个实现SmoothWarmingUp,就不是令牌桶了,而是漏桶算法。也许是出于简单起见,RateLimiter 中的时间窗口能且仅能为 1s。SmoothBursty 有一个可以放 N 个时间窗口产生的令牌的桶,系统空闲的时候令牌就一直攒着,最好情况下可以扛 N 倍于限流值的高峰而不影响后续请求。RateLimite允许某次请求拿走超出剩余令牌数的令牌,但是下一次请求将为此付出代价,一直等到令牌亏空补上,并且桶中有足够本次请求使用的令牌为止。当某次请求不能得到所需要的令牌时,这时涉及到一个权衡,是让前一次请求干等到令牌够用才走掉呢,还是让它先走掉后面的请求等一等呢?Guava 的设计者选择的是后者,先把眼前的活干了,后面的事后面再说。合理的要求是对我的锻炼,不合理的要求是对我的磨练!原文链接:https://www.cnblogs.com/yefeng654321/articles/11939933.html
-
服务降级:系统有限的资源的合理协调概念:服务降级一般是指在服务器压力剧增的时候,根据实际业务使用情况以及流量,对一些服务和页面有策略的不处理或者用一种简单的方式进行处理,从而释放服务器资源的资源以保证核心业务的正常高效运行。原因: 服务器的资源是有限的,而请求是无限的。在用户使用即并发高峰期,会影响整体服务的性能,严重的话会导致宕机,以至于某些重要服务不可用。故高峰期为了保证核心功能服务的可用性,就需要对某些服务降级处理。可以理解为舍小保大应用场景: 多用于微服务架构中,一般当整个微服务架构整体的负载超出了预设的上限阈值(和服务器的配置性能有关系),或者即将到来的流量预计会超过预设的阈值时(比如双11、6.18等活动或者秒杀活动)服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(退路)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。需要考虑的问题:区分那些服务为核心?那些非核心降级策略(处理方式,一般指如何给用户友好的提示或者操作)自动降级还是手动降服务熔断:应对雪崩效应的链路自我保护机制。可看作降级的特殊情况概念:应对微服务雪崩效应的一种链路保护机制,类似股市、保险丝原因: 微服务之间的数据交互是通过远程调用来完成的。服务A调用服务,服务B调用服务c,某一时间链路上对服务C的调用响应时间过长或者服务C不可用,随着时间的增长,对服务C的调用也越来越多,然后服务C崩溃了,但是链路调用还在,对服务B的调用也在持续增多,然后服务B崩溃,随之A也崩溃,导致雪崩效应服务熔断是应对雪崩效应的一种微服务链路保护机制。例如在高压电路中,如果某个地方的电压过高,熔断器就会熔断,对电路进行保护。同样,在微服务架构中,熔断机制也是起着类似的作用。当调用链路的某个微服务不可用或者响应时间太长时,会进行服务熔断,不再有该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。-应用场景:微服务架构中,多个微服务相互调用出使用需要考虑问题:如何所依赖的服务对象不稳定失败之后如何快速恢复依赖对象,如何探知依赖对象是否恢复服务降级和服务熔断区别触发原因不一样,服务熔断由链路上某个服务引起的,服务降级是从整体的负载考虑管理目标层次不一样,服务熔断是一个框架层次的处理,服务降级是业务层次的处理实现方式不一样,服务熔断一般是自我熔断恢复,服务降级相当于人工控制触发原因不同 服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;一句话:服务熔断是应对系统服务雪崩的一种保险措施,给出的一种特殊降级措施。而服务降级则是更加宽泛的概念,主要是对系统整体资源的合理分配以应对压力。服务熔断是服务降级的一种特殊情况,他是防止服务雪崩而采取的措施。系统发生异常或者延迟或者流量太大,都会触发该服务的服务熔断措施,链路熔断,返回兜底方法。这是对局部的一种保险措施。服务降级是对系统整体资源的合理分配。区分核心服务和非核心服务。对某个服务的访问延迟时间、异常等情况做出预估并给出兜底方法。这是一种全局性的考量,对系统整体负荷进行管理。限流:限制并发的请求访问量,超过阈值则拒绝;降级:服务分优先级,牺牲非核心服务(不可用),保证核心服务稳定;从整体负荷考虑;熔断:依赖的下游服务故障触发熔断,避免引发本系统崩溃;系统自动执行和恢复原文链接:https://zhuanlan.zhihu.com/p/341939685
-
一、微服务三板斧 在开发微服务系统时我们通常会面临着高并发问题的考验,为了保证服务的可用性,通常会使用降级、限流和熔断进行处理。接下来我们介绍下这三个基本的概念:服务熔断、服务降级和服务限流,为后面讲解Alibaba的Sentinel组件打下扎实的基础。 1.1 服务降级 服务降级一般是指在服务器压力剧增的时候,根据实际业务使用情况以及流量,对一些服务和页面有策略的不处理或者用一种简单的方式进行处理,从而释放服务器资源的资源以保证核心业务的正常高效运行。通常原因为服务器的资源是有限的,而请求是无限的。在用户使用即并发高峰期,会影响整体服务的性能,严重的话会导致宕机,以至于某些重要服务不可用。故高峰期为了保证核心功能服务的可用性,就需要对某些服务降级处理。可以理解为舍小保大,通常处理为不让客户端等待而是立即返回一个友好的提示。 服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(兜底处理)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。 1.2 服务熔断 在介绍熔断机制之前,我们需要了解微服务的雪崩效应。在微服务架构中,微服务是完成一个单一的业务功能,这样做的好处是可以做到解耦,每个微服务可以独立演进。但是,一个应用可能会有多个微服务组成,微服务之间的数据交互通过远程过程调用完成。这就带来一个问题,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。 熔断机制是应对雪崩效应的一种微服务链路保护机制。我们在各种场景下都会接触到熔断这两个字。高压电路中,如果某个地方的电压过高,熔断器就会熔断,对电路进行保护。同样,在微服务架构中,熔断机制也是起着类似的作用。当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。 1.3 服务熔断和服务降级的区别 这里主要从三个原因分开进行比较,最后 触发原因:服务熔断由链路上某个服务引起的,也就是说,服务熔断一般是某个服务(下游服务)故障引起的,而服务降级是从整体的负载考虑。服务熔断是应对系统服务雪崩的一种保险措施,给出的一种特殊降级措施。而服务降级则是更加宽泛的概念,主要是对系统整体资源的合理分配以应对压力。 管理目标层次:服务熔断是一个框架层次的处理,服务降级是业务层次的处理。 实现方式:服务熔断一般是自我熔断恢复,调用的降级处理是在客户端进行降级处理(编写相应的兜底方法),而服务降级相当于是在服务端进行的兜底方案控制。 总的来说: 服务熔断是服务降级的一种特殊情况,是防止服务雪崩而采取的措施。系统发生异常或者延迟或者流量太大,都会触发该服务的服务熔断措施,链路熔断,返回兜底方法。这是对局部的一种保险措施。 服务降级是对系统整体资源的合理分配。区分核心服务和非核心服务。对某个服务的访问延迟时间、异常等情况做出预估并给出兜底方法。这是一种全局性的考量,对系统整体负荷进行管理。 1.3 服务限流 在上述两种方式中,最终服务的确可以使用,但是访问的却是缺省的服务,比如服务熔断和服务降级最终都会调用相应的兜底方案返回,也就是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用熔断和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。 限流的目的是通过对并发请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。 一般开发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率);其他还有如限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。 常见的限流算法有:令牌桶、漏桶(Sentinel采用的)。计数器也可以进行简单的限流实现。 漏桶算法(Leaky Bucket)思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。示意图如下: 令牌桶算法(Token Bucket)和漏桶效果一样,但方向相反,更加容易理解。随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务。 令牌桶的另外一个好处是可以方便的改变速度,一旦需要提高速率,则按需提高放入桶中的令牌的速率。一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量。 二、总结 本文主要介绍微服务调用过程中的三个基本概念(服务降级、服务熔断、服务限流)进行介绍,为下节进行Sentinel探索打下基础。 ———————————————— 原文链接:https://blog.csdn.net/dl962454/article/details/122193938
-
1 什么是熔断降级 熔断就是“保险丝”。当出现某些状况时,切断服务,从而防止应用程序不断地尝试执 行可能会失败的操作给系统造成“雪崩”,或者大量的超时等待导致系统卡死。 2:降级的目的 降级的目的是当某个服务提供者发生故障的时候,向调用方返回一个错误响应或者替代 响应。举例子:调用联通接口服务器发送短信失败之后,改用移动短信服务器发送,如果移 动短信服务器也失败,则改用电信短信服务器,如果还失败,则返回“失败”响应;在从推 荐商品服务器加载数据的时候,如果失败,则改用从缓存中加载,如果缓存中也加载失败, 则返回一些本地替代数据。 一:Polly 简单使用 建立一个NETCore控制台项目:安装NuGet包:Install-Package Polly -Version 6.0.1 在Program.cs中添加一下代码,每一个部分分为一种方法 启动项目的方法最好用cmd命令执行也可实例调试,命令执行:dotnet PollyTest.dll(PollyTest.dll代表该项目bin目录下的程序dll); using Polly; using System; namespace PollyTest { class Program { static void Main(string[] args) { #region 异常捕获 第一种方法 //实例化 Policy policy = Policy .Handle<ArgumentException>() //故障 .Fallback(() =>//动作 { Console.WriteLine("执行出错"); }); policy.Execute(() => {//在策略中执行业务代码 //这里是可能会产生问题的业务系统代码 Console.WriteLine("开始任务"); throw new ArgumentException("Hello world!"); Console.WriteLine("完成任务"); }); #endregion #region 重载异常 第二种方法 //如果没有被Handle处理的异常,则会导致未处理异常被抛出。还可以用Fallback的其他重载获取异常信息: Policy policy = Policy .Handle<ArgumentException>() //故障 .Fallback(() =>//动作 { Console.WriteLine("执行出错"); }, ex => { Console.WriteLine(ex); }); policy.Execute(() => { Console.WriteLine("开始任务"); throw new ArgumentException("Hello world!"); Console.WriteLine("完成任务"); }); #endregion #region 返回值 第三种方法 //如果Execute中的代码是带返回值的,那么只要使用带泛型的Policy<T> 类即可: Policy<string> policy = Policy<string> .Handle<Exception>() //故障 .Fallback(() =>//动作 { Console.WriteLine("执行出错"); return "降级的值"; }); string value = policy.Execute(() => { Console.WriteLine("开始任务"); throw new Exception("Hello world!"); Console.WriteLine("完成任务"); return "正常的值"; }); #endregion Console.ReadKey(); } } } 重试处理 using Polly; using System; namespace PollyTest { class Program { static void Main(string[] args) { #region 永久重试 //RetryForever()是一直重试直到成功 //Retry()是重试最多一次; //Retry(n) 是重试最多n次; //WaitAndRetry()可以实现“如果出错等待100ms再试还不行再等150ms秒。 Policy policy = Policy .Handle<Exception>() .Retry(3); policy.Execute(() => { Console.WriteLine("开始任务"); if (DateTime.Now.Second % 10 != 0) { throw new Exception("出错"); } Console.WriteLine("完成任务"); }); #endregion Console.ReadKey(); } } } 短路保护 Circuit Breaker 出现N次连续错误,则把“熔断器”(保险丝)熔断,等待一段时间,等待这段时间内如果再Execute 则直接抛出BrokenCircuitException异常,根本不会再去尝试调用业务代码。等待时间过去之后,再 执行Execute的时候如果又错了(一次就够了),那么继续熔断一段时间,否则就恢复正常。 这样就避免一个服务已经不可用了,还是使劲的请求给系统造成更大压力。 using Polly; using System; namespace PollyTest { class Program { static void Main(string[] args) { #region 短路保护 Policy policy = Policy .Handle<Exception>() .CircuitBreaker(6, TimeSpan.FromSeconds(5));//连续出错6次之后熔断5秒(不会再去尝试执行业务代码)。 while (true) { Console.WriteLine("开始Execute"); try { policy.Execute(() => { Console.WriteLine("开始任务"); throw new Exception("出错"); Console.WriteLine("完成任务"); }); } catch (Exception ex) { Console.WriteLine("execute出错" + ex); } Thread.Sleep(500); } Console.ReadKey(); #endregion } } } 策略封装 可以把多个ISyncPolicy合并到一起执行: policy3= policy1.Wrap(policy2); 执行policy3就会把policy1、policy2封装到一起执行 policy9=Policy.Wrap(policy1, policy2, policy3, policy4, policy5);把更多一起封装。 超时处理 #region 超时处理 //下面代码实现了“出现异常则重试三次,如果还出错就FallBack” Policy policyRetry = Policy.Handle<Exception>() .Retry(3); Policy policyFallback = Policy.Handle<Exception>() .Fallback(() => { Console.WriteLine("降级"); }); //Wrap:包裹。policyRetry在里面,policyFallback裹在外面。 //如果里面出现了故障,则把故障抛出来给外面 Policy policy = policyFallback.Wrap(policyRetry); policy.Execute(() => { Console.WriteLine("开始任务"); if (DateTime.Now.Second % 10 != 0) { throw new Exception("出错"); } Console.WriteLine("完成任务"); }); #endregion 二:AOP 框架基础 要求懂的知识:AOP、Filter、反射(Attribute)。 如果直接使用 Polly,那么就会造成业务代码中混杂大量的业务无关代码。我们使用 AOP (如果不了解 AOP,请自行参考网上资料)的方式封装一个简单的框架,模仿 Spring cloud 中的 Hystrix。 需要先引入一个支持.Net Core 的 AOP,目前我发现的最好的.Net Core 下的 AOP 框架是 AspectCore(国产,动态织入),其他要不就是不支持.Net Core,要不就是不支持对异步方法进行 拦截。MVC Filter GitHub:https://github.com/dotnetcore/AspectCore-Framework 命令:Install-Package AspectCore.Core -Version 0.5.0 在该控制台项目中添加Nuget包,并且新建一个CustomInterceptorAttribute类 using AspectCore.DynamicProxy; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace PollyTest { //安装NuGet包AspectCore.Core 如果vs是2.1版本则包要选择1.2.0 vs是3.0则直接安装即可 public class CustomInterceptorAttribute : AbstractInterceptorAttribute { //每个被拦截的方法中执行 public async override Task Invoke(AspectContext context, AspectDelegate next) { try { Console.WriteLine("Before service call"); await next(context);//执行被拦截的方法 } catch (Exception) { Console.WriteLine("Service threw an exception!"); throw; } finally { Console.WriteLine("After service call"); } } } } 在该控制台项目中新建一个被代理拦截的Person类 public class Person { [CustomInterceptor] public virtual void Say(string msg) { Console.WriteLine("service calling..." + msg); } } 在该项目下的程序入口Program类中写入代码进行测试 static void Main(string[] args) { //创建代理 ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder(); using (IProxyGenerator proxyGenerator = proxyGeneratorBuilder.Build()) { Person p = proxyGenerator.CreateClassProxy<Person>(); p.Say("rupeng.com"); } } 三:创建简单的熔断降级框架 熔断降级初级框架实现 1:在该控制台项目中新建一个HystrixCommandAttribute类 [AttributeUsage(AttributeTargets.Method)] public class HystrixCommandAttribute : AbstractInterceptorAttribute { #region 初级框架 public HystrixCommandAttribute(string fallBackMethod) { this.FallBackMethod = fallBackMethod; } public string FallBackMethod { get; set; } public override async Task Invoke(AspectContext context, AspectDelegate next) { try { await next(context);//执行被拦截的方法 } catch (Exception ex) { //context.ServiceMethod 被拦截的方法。context.ServiceMethod.DeclaringType被拦截方法所在的类 //context.Implementation 实际执行的对象 p //context.Parameters 方法参数值 //如果执行失败,则执行 FallBackMethod var fallBackMethod = context.ServiceMethod.DeclaringType.GetMethod(this.FallBackMethod); //反射调用方法 反射的方法Invoke(); Object fallBackResult = fallBackMethod.Invoke(context.Implementation, context.Parameters); context.ReturnValue = fallBackResult; } } #endregion } 2:将该控制台项目中Person类中添加方法(Person类没有就新建,因为这里已经创建过了是直接在上述的基础上进行添加) public class Person { [HystrixCommand("TengXunBackAsync")] //TengXunBackAsync表示如果小米短信失败调的下一个方法名称 public virtual async Task<string> XiaoMiAsync(string name) { throw new Exception("小米短信出现异常!");//自定义抛出一个异常 测试小米失败是否会跳到腾讯短信 Console.WriteLine("小米短信" + name); return "ok"; } [HystrixCommand("HuaWeiBackAsync")] public virtual async Task<string> TengXunBackAsync(string name) { throw new Exception("腾讯短信出现异常!");//自定义抛出一个异常 测试腾讯失败是否会跳到华为短信 Console.WriteLine("腾讯短信" + name); return "ok"; } public async Task<string> HuaWeiBackAsync(string name) { //如果该方法也想在出现异常时调用另一个方法 那本方法就要加一个virtual的修饰符 Console.WriteLine("华为短信" + name); return "ok"; } } 3:在程序入口Program.cs中进行测试 //创建代理 ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder(); using (IProxyGenerator proxyGenerator = proxyGeneratorBuilder.Build()) { Person p = proxyGenerator.CreateClassProxy<Person>(); p.XiaoMiAsync("rupeng.com"); } 四:细化框架 在HystrixCommandAttribute类中添加细化初级框架的代码 添加NuGet包Microsoft.Extensions.Caching.Memory HystrixCommandAttribute类如下: [AttributeUsage(AttributeTargets.Method)] public class HystrixCommandAttribute : AbstractInterceptorAttribute { #region 细化初级框架 /// <summary> /// 最多重试几次,如果为0则不重试 /// </summary> public int MaxRetryTimes { get; set; } = 0; /// <summary> /// 重试间隔的毫秒数 /// </summary> public int RetryIntervalMilliseconds { get; set; } = 100; /// <summary> /// 是否启用熔断 /// </summary> public bool EnableCircuitBreaker { get; set; } = false; /// <summary> /// 熔断前出现允许错误几次 /// </summary> public int ExceptionsAllowedBeforeBreaking { get; set; } = 3; /// <summary> /// 熔断多长时间(毫秒) /// </summary> public int MillisecondsOfBreak { get; set; } = 1000; /// <summary> /// 执行超过多少毫秒则认为超时(0表示不检测超时) /// </summary> public int TimeOutMilliseconds { get; set; } = 0; /// <summary> /// 缓存多少毫秒(0表示不缓存),用“类名+方法名+所有参数ToString拼接”做缓存Key /// </summary> public int CacheTTLMilliseconds { get; set; } = 0; private IAsyncPolicy policy; //NuGet包Microsoft.Extensions.Caching.Memory private static readonly Microsoft.Extensions.Caching.Memory.IMemoryCache memoryCache = new Microsoft.Extensions.Caching.Memory.MemoryCache(new Microsoft.Extensions.Caching.Memory.MemoryCacheOptions()); /// <summary> ///构造函数 /// </summary> /// <param name="fallBackMethod">降级的方法名</param> public HystrixCommandAttribute(string fallBackMethod, int maxRetryTimes) { this.FallBackMethod = fallBackMethod; this.MaxRetryTimes = maxRetryTimes; } /// <summary> ///构造函数重载 /// </summary> /// <param name="fallBackMethod">降级的方法名</param> public HystrixCommandAttribute(string fallBackMethod) { this.FallBackMethod = fallBackMethod; } public string FallBackMethod { get; set; } public override async Task Invoke(AspectContext context, AspectDelegate next) { //一个HystrixCommand中保持一个policy对象即可 //其实主要是CircuitBreaker要求对于同一段代码要共享一个policy对象 //根据反射原理,同一个方法就对应一个HystrixCommandAttribute,无论几次调用, //而不同方法对应不同的HystrixCommandAttribute对象,天然的一个policy对象共享 //因为同一个方法共享一个policy,因此这个CircuitBreaker是针对所有请求的。 //Attribute也不会在运行时再去改变属性的值,共享同一个policy对象也没问题 lock (this)//因为Invoke可能是并发调用,因此要确保policy赋值的线程安全 { if (policy == null) { policy = Policy.NoOpAsync();//创建一个空的Policy //如果启用熔断 if (EnableCircuitBreaker) { //策略封装 policy = policy.WrapAsync(Policy.Handle<Exception>().CircuitBreakerAsync(ExceptionsAllowedBeforeBreaking, TimeSpan.FromMilliseconds(MillisecondsOfBreak))); } //超时时间 if (TimeOutMilliseconds > 0) { //策略封装执行超时 policy = policy.WrapAsync(Policy.TimeoutAsync(() => TimeSpan.FromMilliseconds(TimeOutMilliseconds), Polly.Timeout.TimeoutStrategy.Pessimistic)); } //最大重试次数 if (MaxRetryTimes > 0) { //策略封装最多试几次 policy = policy.WrapAsync(Policy.Handle<Exception>().WaitAndRetryAsync(MaxRetryTimes, i => TimeSpan.FromMilliseconds(RetryIntervalMilliseconds))); } IAsyncPolicy policyFallBack = Policy .Handle<Exception>() .FallbackAsync(async (ctx, t) => { //这里拿到的就是ExecuteAsync(ctx => next(context), pollyCtx);这里传的pollyCtx AspectContext aspectContext = (AspectContext)ctx["aspectContext"]; //获取降级的方法 var fallBackMethod = context.ServiceMethod.DeclaringType.GetMethod(this.FallBackMethod); //调用降级的方法 Object fallBackResult = fallBackMethod.Invoke(context.Implementation, context.Parameters); //不能如下这样,因为这是闭包相关,如果这样写第二次调用Invoke的时候context指向的 //还是第一次的对象,所以要通过Polly的上下文来传递AspectContext //context.ReturnValue = fallBackResult; aspectContext.ReturnValue = fallBackResult; }, async (ex, t) => { }); policy = policyFallBack.WrapAsync(policy); } } //把本地调用的AspectContext传递给Polly,主要给FallbackAsync中使用,避免闭包的坑 Context pollyCtx = new Context();//Context是polly中通过Execute给FallBack、Execute等回调方法传上下文对象使用的 pollyCtx["aspectContext"] = context;//context是aspectCore的上下文 //Install-Package Microsoft.Extensions.Caching.Memory if (CacheTTLMilliseconds > 0) { //用类名+方法名+参数的下划线连接起来作为缓存key string cacheKey = "HystrixMethodCacheManager_Key_" + context.ServiceMethod.DeclaringType + "." + context.ServiceMethod + string.Join("_", context.Parameters); //尝试去缓存中获取。如果找到了,则直接用缓存中的值做返回值 if (memoryCache.TryGetValue(cacheKey, out var cacheValue)) { context.ReturnValue = cacheValue; } else { //如果缓存中没有,则执行实际被拦截的方法 await policy.ExecuteAsync(ctx => next(context), pollyCtx); //存入缓存中 using (var cacheEntry = memoryCache.CreateEntry(cacheKey)) { cacheEntry.Value = context.ReturnValue;//返回值放入缓存 cacheEntry.AbsoluteExpiration = DateTime.Now + TimeSpan.FromMilliseconds(CacheTTLMilliseconds); } } } else//如果没有启用缓存,就直接执行业务方法 { await policy.ExecuteAsync(ctx => next(context), pollyCtx); } } #endregion } 将该控制台项目中Person类中修改方法 public class Person { [HystrixCommand("TengXunBackAsync",3)] //("TengXunBackAsync",3)第一个参数表示如果小米短信失败调的下一个方法,第二个表示如果失败重试的次数 //更多参数属性的使用效果可参照该项目HystrixCommandAttribute类中属性 public virtual async Task<string> XiaoMiAsync(string name) { Console.WriteLine("小米短信"); throw new Exception("小米短信出现异常!");//如果在规定的请求数类全部失败则会进入下一个方法 本次测试时测试三次重试 Console.WriteLine("小米短信" + name); return "ok"; } [HystrixCommand("HuaWeiBackAsync",3)] public virtual async Task<string> TengXunBackAsync(string name) { Console.WriteLine("腾讯短信"); throw new Exception("腾讯短信出现异常!"); Console.WriteLine("腾讯短信" + name); return "ok"; } public async Task<string> HuaWeiBackAsync(string name) { //如果该方法也想在出现异常时调用另一个方法 那本方法就要加一个virtual的修饰符 Console.WriteLine("华为短信" + name); return "ok"; } } 最后Program.cs中进行测试 //创建代理 ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder(); using (IProxyGenerator proxyGenerator = proxyGeneratorBuilder.Build()) { Person p = proxyGenerator.CreateClassProxy<Person>(); p.XiaoMiAsync("rupeng.com"); } Console.ReadKey(); 文章知识点与官方知识档案匹配,可进一步学习相关知识 ———————————————— 原文链接:https://blog.csdn.net/weixin_44481764/article/details/102947204
-
1、引言在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以相互调用(RPC)。为了保证其高可用,单个服务通常会集群部署。由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这种现象就是“服务雪崩”。1.png如上图图所示:A作为服务提供者,B为A的服务消费者,C和D是B的服务消费者。A不可用引起了B的不可用,并将不可用像滚雪球一样放大到C和D时,雪崩效应就形成了。另外,在大中型分布式系统中,通常系统很多依赖(HTTP,hession,Netty,Dubbo等),如下图:2.png在高并发访问下,这些依赖的稳定性与否对系统的影响非常大,但是依赖有很多不可控问题:如网络连接缓慢,资源繁忙,暂时不可用,服务脱机等。如下图:QPS为50的依赖 I 出现不可用,但是其他依赖仍然可用:3.png当依赖I 阻塞时,大多数服务器的线程池就出现阻塞,影响整个线上服务的稳定性。如下图:4.png在复杂的分布式架构的应用程序有很多的依赖,都会不可避免地在某些时候失败。高并发的依赖失败时如果没有隔离措施,当前应用服务就有被拖垮的风险。很多服务框架会提供服务重试机制,在很大程度上解决了由于网络瞬时不可达的问题。但是在很多的情况下造成服务雪崩的元凶正是服务重试机制。某个服务本来就已经出现问题了,造成资源占用无法释放、请求延时等问题。这时在请求失败之后又不断的发送重试请求,在原本就无法释放的资源基础上继续膨胀式占用,导致整个系统资源耗尽。导致服务雪崩。要防止系统发生雪崩,就必须要有容错设计。如果遇到突增流量,一般的做法是对非核心业务功能采用熔断和降级的措施来保护核心业务功能正常服务,而对于核心功能服务,则需要采用限流的措施。2、熔断熔断来自英文短语circuit breaker,circuit breaker在电工学里就是断路器的意思。它就相当于一个开关,打开后可以阻止流量通过。比如保险丝,当电流过大时,就会熔断,从而避免元器件损坏。服务熔断是指调用方访问服务时通过断路器做代理进行访问,断路器会持续观察服务返回的成功、失败的状态,当失败超过设置的阈值时断路器打开,请求就不能真正地访问到服务了。5.jpeg一般而言,熔断状态不会一直持续,而是有一个时间范围,时间过了以后再去尝试请求服务提供者,一旦服务提供者的服务能力恢复,请求将继续可以调用服务提供者。断路器的状态转换图如下:6.png断路器默认处于“关闭”状态,当服务提供者的错误率到达阈值,就会触发断路器“开启”。断路器开启后进入熔断时间,到达熔断时间终点后重置熔断时间,进入“半开启”状态在半开启状态下,如果服务提供者的服务能力恢复,则断路器关闭熔断状态。进而进入正常的服务状态。在半开启状态下,如果服务提供者的服务能力未能恢复,则断路器再次触发服务熔断,进入熔断时间。3、降级服务熔断通常适与服务降级配合使用。在服务发生熔断后,一般会让请求走事先配置的处理方法,这个处理方法就是一个降级逻辑。简言之,服务降级是一种兜底的服务策略,体现了一种“实在不行就怎么这么样”的思想。想去北京买不到飞机票,实在不行就坐高铁去吧;感冒了想去看病挂不上号,实在不行就先回家吃点药睡一觉吧;实在不行之后的处理方法,被称为fallback方法。当服务提供者故障触发调用者服务的熔断机制,服务调用者就不再调用远程服务方法,而是调用本地的fallback方法。此时你需要预先提供一个处理方法,作为服务降级之后的执行方法,fallback返回值一般是设置的默认值或者来自缓存。7.png除了可以在服务调用端实现服务降级,还可以在服务提供端实现服务降级。8.png实际上在大型的微服务系统中,服务提供者和服务消费者并没有严格的区分,很多的服务既是提供者,也是消费者。4、限流4.1 为什么需要限流举一个我们生活中的例子:一些热门的旅游景点,往往会对每日的旅游参观人数有严格的限制,比如厦门的鼓浪屿、北京的故宫等,每天只会卖出固定数目的门票,如果你去的晚了,可能当天的票就已经卖完了,当天就无法进去游玩了。为什么旅游景点要做这样的限制呢?多卖一些门票多赚一些钱岂不是更好?原因在于景点的服务资源是有限的,每日能服务的人数是有限的,一旦放开限制了,景点的工作人员就会不够用,卫生情况也得不到保障,安全也有隐患,超密集的人群也会严重的影响游客的体验。 但由于景区名气大,来游玩的旅客络绎不绝,远超出了景区的承载能力,因此景区只好做出限制每日人员流量的举措。同理,在IT软件行业中,系统服务也是这样的。服务限流是指当系统资源不够,不足以应对大量请求,即系统资源与访问量出现矛盾的时候,我们为了保证有限的资源能够正常服务,因此对系统按照预设的规则进行流量限制或功能限制的一种方法。如果你的系统理论是时间单位内可服务100W用户,但是今天却突然来了300W用户,由于用户流量的随机性,如果不加以限流,很有可能这300W用户一下子就压垮了系统,导致所有人都得不到服务。因此为了保证系统至少还能为100W用户提供正常服务,我们需要对系统进行限流设计。有的人可能会想,既然会有300W用户来访问,那为啥系统不干脆设计成能足以支撑这么大量用户的集群呢?这是个好问题。如果系统是长期有300W的用户来访问,肯定是要做上述升级的,但是常常面临的情况是,系统的日常访问量就是100W,只不过偶尔有一些不可预知的特定原因导致的短时间的流量激增,这个时候,公司往往出于节约成本的考虑,不会为了一个不常见的尖峰来把我们的系统扩容到最大的尺寸。转载自作者:冰河winner链接:https://www.jianshu.com/p/5d2b7cea1f11
-
这几年来,微服务这个概念越来越火了,火到什么程度呢?2019年有一个统计说,两千家企业里,45%在使用微服务,16%在实验开发和测试微服务架构,24%在学习微服务准备转型,只有剩下的15%的企业没有使用微服务。微服务到底有什么好呢?微服务在2013年才被提出,短短几年就有这么快速的发展。微服务架构能够实现由小型自主服务组成一个整体应用,各个组成部分之间是松耦合的,复杂性低,各个部分可以独立部署,修复bug或者引入新特性更容易,能够独立扩展,不同技术栈之间可以使用不同框架、不同版本库甚至不同的操作系统平台。对于中大型架构系统来说,微服务更加便捷,微服务成为很多企业架构重构的方向,同时也对架构师提出更高的挑战。目前有很多常用于微服务构建的框架,对于构建微服务架构能够带来一些帮助。Java语言相关微服务框架 Spring Boot Spring Boot的设计目的是简化新Spring应用初始搭建以及开发过程,2017年有64.4%的受访者决定使用Spring Boot,可以说是最受欢迎的微服务开发框架。利用Spring Boot开发的便捷度简化分布式系统基础设施的开发,比如像配置中心、注册、负载均衡等方面都可以做到一键启动和一键部署。 Spring Cloud Spring Cloud是一个系列框架的合计,基于HTTP(s)的RETS服务构建服务体系,Spring Cloud能够帮助架构师构建一整套完整的微服务架构技术生态链。Dubbo Dubbo是由阿里巴巴开源的分布式服务化治理框架,通过RPC请求方式访问。Dubbo是在阿里巴巴的电商平台中逐渐探索演进所形成的,经历过复杂业务的高并发挑战,比Spring Cloud的开源时间还要早。目前阿里、京东、当当、携程、去哪等一些企业都在使用Dubbo。Dropwizard Dropwizard将Java生态系统中各个问题域里最好的组建集成于一身,能够快速打造一个Rest风格的后台,还可以整合Dropwizard核心以外的项目。国内现在使用Dropwizard还很少,资源也不多,但是与Spring Boot相比,Dropwizard在轻量化上更有优势,同时如果用过Spring,那么基本也会使用Spring Boot。 Akka Akka是一个用Scala编写的库,可以用在有简化编写容错、高可伸缩性的Java和Scala的Actor模型,使用Akka能够实现微服务集群。Vert.x/Lagom/ReactiveX/Spring 5 这四种框架主要用于响应式微服务开发,响应式本身和微服务没有关系,更多用于提升性能上,但是可以和微服务相结合,也可以提升性能。.Net相关微服务框架 .NET Core .NET Core是专门针对模块化微服务架构设计的,是跨平台应用程序开发框架,是微软开发的第一个官方版本。 Service Fabric Service Fabric是微软开发的一个微服务框架,基于Service Fabric构建的很多云服务被用在了Azure上。 Surging Surging是基于RPC协议的分布式微服务技术框架,基于.NET Core而来。 Microdot Framework Microdot Framework用于编写定义服务逻辑代码,不需要解决开发分布式系统的挑战,能够很方便的进行MicrosoftOrleans集成。Node.js相关微服务框架 Seneca Seneca是Node.js的微服务框架开发工具,可以用于编写可用于产品环境的代码。 Hapi/Restify/LoopBack 这三种框架的分工不同,前两种更适合开发简单的微服务后端系统,第三种更适合用在大型复杂应用开发,还可以用在现有微服务上的构建。Go相关微服务框架 Go-Kit/Goa/Dubbogo Go-Kit是分布式开发的工具合集,适合用于大型业务场景下构建微服务;Goa是用Go语言构建的微服务框架;Dubbogo是和阿里巴巴开源的Dubbo能够兼容的Golang微服务框架。Python相关微服务框架Python相关的微服务框架非常少,用的比较多的是Nameko。Nameko让实现微服务变得更简单,同时也提供了很丰富的功能,比如支持负载均衡、服务发现还支持依赖自动注入等,使用起来很方便,但是有限速、超时和权限机制不完善等缺点。总结微服务已经成为很多大型互联网公司的选择,对于架构师和想要成为架构师的工程师来说,掌握微服务不仅要学会使用相关框架来实现,还要掌握具体用法,在具体的实践中仍然要避开很多坑。原文链接:https://blog.csdn.net/qq_37217713/article/details/108114441
-
基础 单体架构:将业务全部功能集中到一个项目中,打成一个war包存储,部署在一台服务器中,只有一个数据库 优点 :架构简单,部署成本低。适合小型项目 问题:高并发性能问题,开发时代码耦合问题,部署升级时停服的问题 垂直架构:拆分模块,每个模块使用自己的数据库,如果有模块需要其他模块数据时需要自己查对方模块数据库 问题:大量代码冗余,系统难以维护,性能问题,部署问题 分布式架构:根据业务功能对系统做拆分,每个业务功能作为独立项目开发,称为一个服务 服务之间相互调用,分布式多节点部署 优点:降低耦合,有利于服务升级和拓展 适合大型互联网项目 缺点:服务调用关系错综复杂 服务拆分的粒度如何界定? 服务之间如何调用? 服务的调用关系如何管理? SOA架构Service-Oriented Architecture,即面向服务的架构,它可以根据需求通过网络对松散耦合的粗粒度应用组件(服务)进行分布式部署、组合和使用。一个服务通常以独立的形式存在于操作系统进程中。 特点:分布式、可重用、扩展灵活、松耦合 解决的问题: 系统集成 企业系统不断发展过程中,会存在N多系统间相互调用,系统间的关系可能会是一个比较杂乱的网状结构。引入SOA来完成服务之间关系的有序化,这一步需要引入一些产品,比如 ESB、以及技术规范、服务管理规范 系统服务化 完成服务的复用。比如之前可能是 各个系统都写了一套登录注册、发邮件、发短信等功能。现在可以将 登录注册、发邮件、发短信等功能逻辑抽象成可复用、可组装的服务,通过合理的服务编排,实现业务功能的快速复用。 业务服务化 完成企业系统的对外服务能力。将业务单元(如:OA系统、财务系统等)封装成一项服务。 优点: 抽取公共的功能为服务,提高开发效率 对不同的服务进行集群化部署解决系统压力 基于ESB/DUBBO减少系统耦合 缺点: 抽取服务的粒度较大 服务提供方与调用方接口耦合度较高 微服务架构 微服务 微服务不是一种框架,而且一种架构思想。微服务架构的系统是个分布式系统,按业务领域划分为独立的服务单元,有自动化运维、容错、快速演进的特点,它能够解决传统单体架构系统的痛点,同时也能满足越来越复杂的业务需求。简而言之,微服务架构的风格,就是将单一程序开发成一个微服务,每个微服务运行在自己的进程中,并使用轻量级通信机制,通常是HTTP 、 RESTFUL 、API 。这些服务围绕业务能力来划分构建的,并通过完全自动化部署机制来独立部署这些服务,这些服务可以使用不同的编程语言,以及不同的数据存储技术,以保证最低限度的集中式管理。 理解:就是将一个大的应用拆分为多个小的模块,每个模块都有自己的功能和职责,每个模块可以进行交互,这就是微服务。 1.单一职责,每一个服务对应唯一的业务能力,做到单一职责 2.自治,团队独立,技术独立,数据独立,独立部署和交付 3.面向服务,服务提供统一的接口,与语言技术无关 4.隔离性强,服务调用做好隔离,容错,降级避免出现级联问题(级联故障是由于正反馈循环并且随着时间的增加所产生的故障。典型表现:最初由单个节点或子系统故障触发的连锁反应) 微服务进一步降低服务之间的耦合,提供服务的独立性和灵活性。微服务是一种经过良好架构设计的分布式架构方案。 背景 单体架构不足 1.业务越来越复杂,单体应用的代码量越来越大,代码的可读性、可维护性和可扩展性下降,新人接手代码所需的时间成倍增加,业务扩展带来的代价越来越大。 2.随着用户越来越多,程序承受的并发越来越高,单体应用的并发能力有限。 3.测试的难度越来越大,单体应用的业务都在同个程序中,随着业务的扩张、复杂度的增加,单体应用修改业务或者增加业务或许会给其他业务带来定的影响,导致测试难度增加。 微服务技术的对比 CAP 理论 CAP 也就是 Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性) 这三个单词首字母组合。 CAP 理论的提出者布鲁尔在提出 CAP 猜想的时候,并没有详细定义 Consistency、Availability、Partition Tolerance 三个单词的明确定义。 在理论计算机科学中,CAP 定理(CAP theorem)指出对于一个分布式系统来说,当设计读写操作时,只能同时满足以下三点中的两个: 一致性(Consistency) : 所有节点访问同一份最新的数据副本 可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。 分区容错性(Partition Tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。 什么是网络分区? 分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫 网络分区。 当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。 简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。 因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。 比如 ZooKeeper、HBase 就是 CP 架构,Cassandra、Eureka 就是 AP 架构,Nacos 不仅支持 CP 架构也支持 AP 架构。 为啥不可能选择 CA 架构呢? 举个例子:若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。 选择 CP 还是 AP 的关键在于当前的业务场景,没有定论,比如对于需要确保强一致性的场景如银行一般会选择保证 CP 。 另外,需要补充说明的一点是: 如果网络分区正常的话(系统在绝大部分时候所处的状态),也就说不需要保证 P 的时候,C 和 A 能够同时保证。 常见的可以作为注册中心的组件有:ZooKeeper、Eureka、Nacos...。 ZooKeeper 保证的是 CP。 任何时刻对 ZooKeeper 的读请求都能得到一致性的结果,但是, ZooKeeper 不保证每次请求的可用性比如在 Leader 选举过程中或者半数以上的机器不可用的时候服务就是不可用的。 Eureka 保证的则是 AP。 Eureka 在设计的时候就是优先保证 A (可用性)。在 Eureka 中不存在什么 Leader 节点,每个节点都是一样的、平等的。因此 Eureka 不会像 ZooKeeper 那样出现选举过程中或者半数以上的机器不可用的时候服务就是不可用的情况。 Eureka 保证即使大部分节点挂掉也不会影响正常提供服务,只要有一个节点是可用的就行了。只不过这个节点上的数据可能并不是最新的。 Nacos 不仅支持 CP 也支持 AP。 总结 在进行分布式系统设计和开发时,我们不应该仅仅局限在 CAP 问题上,还要关注系统的扩展性、可用性等等 在系统发生“分区”的情况下,CAP 理论只能满足 CP 或者 AP。要注意的是,这里的前提是系统发生了“分区” 如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。 总结:如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA BASE理论 BASE 是 Basically Available(基本可用) 、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。 BASE 理论的核心思想 即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 也就是牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。 BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。 如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。因此,如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。 因此,AP 方案只是在系统发生分区的时候放弃一致性,而不是永远放弃一致性。在分区故障恢复后,系统应该达到最终一致性。这一点其实就是 BASE 理论延伸的地方。 基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。 什么叫允许损失部分可用性呢? 响应时间上的损失: 正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。 系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。 软状态 软状态指允许系统中的数据存在中间状态(CAP 理论中的数据不一致),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。 最终一致性 最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。 分布式一致性的 3 种级别: 强一致性 :系统写入了什么,读出来的就是什么。 弱一致性 :不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。 最终一致性 :弱一致性的升级版,系统会保证在一定时间内达到数据一致的状态。 业界比较推崇是最终一致性级别,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。 读时修复 : 在读取数据时,检测数据的不一致,进行修复。比如 Cassandra 的 Read Repair 实现,具体来说,在向 Cassandra 系统查询数据的时候,如果检测到不同节点 的副本数据不一致,系统就自动修复数据。 写时修复 : 在写入数据,检测数据的不一致时,进行修复。比如 Cassandra 的 Hinted Handoff 实现。具体来说,Cassandra 集群的节点之间远程写数据的时候,如果写失败 就将数据缓存下来,然后定时重传,修复数据的不一致性。 异步修复 : 这个是最常用的方式,通过定时对账检测副本数据的一致性,并修复。 比较推荐 写时修复,这种方式对性能消耗比较低。 总结 ACID 是数据库事务完整性的理论,CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 方案的延伸。 SpringCloud SpringCloud(微服务框架)集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。 版本对应:https://start.spring.io/actuator/info ①Spring Cloud 是一系列框架的有序集合。 ②Spring Cloud 并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来。 netflix eureka 1.1,alibaba 2.2 ③通过 Spring Boot 风格进行再封装,屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。 ④它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、 断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。 ⑤Spring Cloud项目官方网址:https://spring.io/projects/spring-cloud ⑥Spring Cloud 版本命名方式采用了伦敦地铁站的名称,同时根据字母表的顺序来对应版本时间顺序,比如:最早的Release版本:Angel,第二个Release版本:Brixton,然后是Camden、Dalston、Edgware,Finchley,Greenwich,Hoxton springcloud组件: 红色不维护。绿色是alibaba一套,推荐使用。 统一网管路由 SpringCloudGateway,Zuul 服务链路监控 Zipkin,Sleuth 注册发现 服务发现:通过服务的应用名称找到服务的具体实例的过程 根据服务名称发现服务的实例过程 客户端会在本地缓存服务端的列表 拉取列表是有间隔周期的 (导致服务上线 客户端不能第一时间感知到 (可以容忍)) 其实每次做服务发现 都是从本地的列表来进行的 Redis 怎么清除过期的 key LRU(热点 key) 1 定时(k-thread) 2 惰性 (在再次访问该 key 时有作用) 3 定期 (使用一个线程来完成清除任务) 定期(实时性差) + 惰性 Eureka-Server Eureka-Server 不仅提供让别人注册的功能,它也能注册到别人里面,自己注册自己,默认会注册自己,我们也可以关掉这个功能。 常用配置文件设置 服务端常用 客户端常用 原理 Eureka-server 对外提供的是 restful 风格的服务 以 http 动词的形式对 url 资源进行操作 get post put delete http 服务 + 特定的请求方式 + 特定的 url 地址 只要利用这些 restful 我们就能对项目实现注册和发现。只不过,eureka 已经帮我们使用 java 语言写了 client,让我们的项目只要依赖 client 就能实现注册和发现!只要你会发起 Http 请求,那你就有可能自己实现服务的注册和发现。不管你是什么语言! 集群 nacos 负载均衡 负载均衡(Load Balance,简称 LB)是高并发、高可用系统必不可少的关键组件,目标是尽力将网络流量平均分发到多个服务器上,以提高系统整体的响应速度和可用性。什么是负载均衡,看完文章秒懂_爱铭网络的博客-CSDN博客_负载均衡 高并发:负载均衡通过算法调整负载,尽力均匀的分配应用集群中各节点的工作量,以此提高应用集群的并发处理能力(吞吐量)。 伸缩性:添加或减少服务器数量,然后由负载均衡进行分发控制。这使得应用集群具备伸缩性。 高可用:负载均衡器可以监控候选服务器,当服务器不可用时,自动跳过,将请求分发给可用的服务器。这使得应用集群具备高可用的特性。 安全防护:有些负载均衡软件或硬件提供了安全性功能,如:黑白名单处理、防火墙,防 DDos 攻击等。 常见算法 1.RoundRobinRule--轮询 请求次数 % 机器数量。把每个请求轮流发送到每个服务器上。 2.RandomRule--随机。该算法比较适合服务器性能差不多的场景。 3.加权轮询(Weighted Round Robbin)加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。 4. 源地址哈希(IP Hash) 根据请求源 IP,通过哈希计算得到一个数值,用该数值在候选服务器列表的进行取模运算,得到的结果便是选中的服务器。 3.最小活跃数(Least Active)算法 将请求分发到连接数/请求数最少的候选服务器(目前处理请求最少的服务器)。 4.Weighted ResponseTimeRule--根据平均响应时间计算所有服务的权重,响应时间越快服务权重越大被选中的概率越大。刚启动时如果同统计信息不足,则使用轮询的策略,等统计信息足够会切换到自身规则 5.RetryRule-- 先按照轮询的策略获取服务,如果获取服务失败则在指定的时间内会进行重试,获取可用的服务 6.BestAvailableRule --会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量小的服务 7.ZoneAvoidanceRule -- 默认规则,复合判断 Server 所在区域的性能和 Server 的可用行选择服务器。 8.一致性哈希(Consistent Hash)算法的目标是:相同的请求尽可能落到同一个服务器上。 9.AvailabilityFilteringRule --会先过滤掉由于多次访问故障处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对于剩余的服务列表按照轮询的策略进行访问 Ribbon 默认使用哪一个负载均衡算法: ZoneAvoidanceRule :区间内亲和轮询的算法!通过一个 key 来区分 常见的负载均衡方式: 服务端负载均衡 客户端负载均衡 服务端负载均衡 需要建立一个独立的负载均衡服务器。 负载均衡是在客户端发送请求后进行的,因此客户端并不知道到底是哪个服务端提供的服务。 可用服务端清单存储在负载均衡服务器上。 客户端负载均衡:相较于服务端负载均衡,客户端服务在均衡则是一个比较小众的概念。 客户端负载均衡也需要心跳机制去维护服务端清单的有效性,这个过程需要配合服务注册中心一起完成。 负载均衡器位于客户端,不需要单独搭建一个负载均衡服务器。 负载均衡是在客户端发送请求前进行的,因此客户端清楚地知道是哪个服务端提供的服务。 客户端都维护了一份可用服务清单,而这份清单都是从服务注册中心获取的。 Ribbon 就是一个基于 HTTP 和 TCP 的客户端负载均衡器,当我们将 Ribbon 和 Eureka 一起使用时,Ribbon 会从 Eureka Server(服务注册中心)中获取服务端列表,然后通过负载均衡策略将请求分摊给多个服务提供者,从而达到负载均衡的目的。 ribbon Ribbon 要做什么事情? 先通过 "http://" + serviceId + "/info" 我们思考 ribbon 在真正调用之前需要做什么? restTemplate.getForObject(“http://provider/info”, String.class); 1. 拦截该请求; 2. 获取该请求的 URL 地址:http://provider/info 3. 截取 URL 地址中的 provider 4. 从服务列表中找到 key 为 provider 的服务实例的集合(服务发现) 5. 根据负载均衡算法选出一个符合的实例 6. 拿到该实例的 host 和 port,重构原来 URL 中的 provider 7. 真正的发送 restTemplate.getForObject(“http://ip:port/info”,String.class) Ribbon 源码核心: ILoadBalancer 接口:起到承上启下的作用 1. 承上:从 eureka 拉取服务列表 2. 启下:使用 IRule 算法实现客户端调用的负载均衡 设计思想:每一个服务提供者都有自己的 ILoadBalancer userService---》客户端有自己的 ILoadBalancer TeacherService---》客户端有自己的 ILoadBalancer 在客户端里面就是 Map<String,ILoadBalancer> iLoadBalancers Map<String,ILoadBalancer> iLoadBalancers 消费者端 服务提供者的名称 value (服务列表 算法规则 ) 如何实现负载均衡的呢? iloadBalancer loadbalance = iloadBalancers.get(“user-service”) List<Server> servers = Loadbalance.getReachableServers();//缓存起来 Server server = loadbalance .chooseServer(key) //key 是区 id,--》IRule 算法 chooseServer 下面有一个 IRule 算法 IRule 下面有很多实现的负载均衡算法 负载均衡之前的服务列表是从何而来呢? 从 eureka 来 Ribbon 里面有没有服务列表?Ribbon 有一个核心接口 ILoadBalance(承上(eureka)启下(Rule)) Ribbon 只做负载均衡和远程调用 我们发现在负载均衡之前,服务列表已经有数据了 loadbalance 服务接口调用 OpenFeign Feign 的英文表意为“假装,伪装,变形”, 是一个http请求调用的轻量级框架,是以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。 Feign 是一个声明式的 REST 客户端,它用了基于接口的注解方式,很方便实现客户端像调用本地接口方法一样,进行远程调用。 Feign 最初由 Netflix 公司提供,但不支持SpringMVC注解,后由 SpringCloud 对其封装,支持了SpringMVC注解,让使用者更易于接受。 Spring Cloud 常见的集成方式是使用Feign+Ribbon技术来完成服务间远程调用及负载均衡的 其实Feign的一个机制就是使用了动态代理: 首先,如果你对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理 接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心 Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址 最后针对这个地址,发起请求、解析响应 流程: 在微服务启动时,会向注册中心注册自身实例信息; 微服务会定时向服务发现中心获取服务实例列表,保存到本地; 当ServiceA调用ServiceB时,ribbon组件从本地服务实例列表中查找serviceB实例,如获取了多个实例如:Instance1、Instance2。这时ribbon会通过用户所配置的负载均衡策略从中选择一个实例。 最终,Feign组件会通过ribbon选取的实例发送http请求。 采用Feign+Ribbon的整合方式,是由Feign完成远程调用的整个流程。而Feign集成了Ribbon,Feign使用Ribbon 1. OpenFeign 用过吗?它是如何运作的? 在主启动类上加上@EnableFeignClients 注解后,启动会进行包扫描,把所有加了@FeignClient(value=”xxx-service”)注解的接口进行创建代理对象通过代理对象,使用ribbon 做了负载均衡和远程调用 2. 如何创建的代理对象? 当 项 目在启动时,先扫描,然后拿到标记了@FeignClient 注 解的接口信息 ,由ReflectiveFeign 类的 newInstance 方法创建了代理对象JDK代理 3. OpenFeign 到底是用什么做的远程调用? 使用的是 HttpURLConnection (java.net) 4. OpenFeign 怎么和 ribbon 整合的? 在代理对象执行调用的时候 断路器 -保险丝 三板斧:服务降级、服务熔断、服务限流 服务降级 通常原因为服务器的资源是有限的,而请求是无限的。在用户使用即并发高峰期,会影响整体服务的性能,严重的话会导致宕机,以至于某些重要服务不可用。故高峰期为了保证核心功能服务的可用性,就需要对某些服务降级处理。可以理解为舍小保大,通常处理为不让客户端等待而是立即返回一个友好的提示。 服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(兜底处理)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。 服务熔断 在微服务架构中,微服务是完成一个单一的业务功能,这样做的好处是可以做到解耦,每个微服务可以独立演进。但是,一个应用可能会有多个微服务组成,微服务之间的数据交互通过远程过程调用完成。这就带来一个问题,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。 线程没有及时回收 熔断机制是应对雪崩效应的一种微服务链路保护机制。我们在各种场景下都会接触到熔断这两个字。高压电路中,如果某个地方的电压过高,熔断器就会熔断,对电路进行保护。同样,在微服务架构中,熔断机制也是起着类似的作用。当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。 服务熔断和服务降级的区别 这里主要从三个原因分开进行比较,最后 触发原因:服务熔断由链路上某个服务引起的,也就是说,服务熔断一般是某个服务(下游服务)故障引起的,而服务降级是从整体的负载考虑。服务熔断是应对系统服务雪崩的一种保险措施,给出的一种特殊降级措施。而服务降级则是更加宽泛的概念,主要是对系统整体资源的合理分配以应对压力。 管理目标层次:服务熔断是一个框架层次的处理,服务降级是业务层次的处理。 实现方式:服务熔断一般是自我熔断恢复,调用的降级处理是在客户端进行降级处理(编写相应的兜底方法),而服务降级相当于是在服务端进行的兜底方案控制。 总的来说: 服务熔断是服务降级的一种特殊情况,是防止服务雪崩而采取的措施。系统发生异常或者延迟或者流量太大,都会触发该服务的服务熔断措施,链路熔断,返回兜底方法。这是对局部的一种保险措施。 服务降级是对系统整体资源的合理分配。区分核心服务和非核心服务。对某个服务的访问延迟时间、异常等情况做出预估并给出兜底方法。这是一种全局性的考量,对系统整体负荷进行管理。 服务限流 在上述两种方式中,最终服务的确可以使用,但是访问的却是缺省的服务,比如服务熔断和服务降级最终都会调用相应的兜底方案返回,也就是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用熔断和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。 限流的目的是通过对并发请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。 一般开发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率);其他还有如限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。 常见的限流算法有:令牌桶、漏桶(Sentinel采用的)。计数器也可以进行简单的限流实现。 Hystrix(豪猪) Hystix 是 Netflix 开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败(雪崩)。 Hystrix是隔离、熔断以及降级的一个框架,说白了就是Hystrix会搞很多小线程池然后让这些小线程池去请求服务,返回结果,Hystrix相当于是个中间过滤区,如果我们的积分服务挂了,那我们请求积分服务直接就返回了,不需要等待超时时间结束抛出异常,这就是所谓的熔断,但是也不能啥都不干就返回啊,不然我们之后手动加积分咋整啊,那我们每次调用积分服务就在数据库里记录一条消息,这就是所谓的降级,Hystrix隔离、熔断和降级的全流程如下: Sentinel(哨兵) 网关 • 网关旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。 • 在微服务架构中,不同的微服务可以有不同的网络地址,各个微服务之间通过互相调用完成用户请求,客户端可能通过调用N个微服务的接口完成一个用户请求。 • 网关就是系统的入口,封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、缓存、负载均衡、流量管控、路由转发等 • 在目前的网关解决方案里,有Nginx+ Lua、Netflix Zuul/zuul2 、Spring Cloud Gateway等等 微服务背景下,一个系统被拆分为多个服务,但是像安全认证,流量控制,日志,监控等功能是每个服务都需要的,没有网关的话,我们就需要在每个服务中单独实现,这使得我们做了很多重复的事情并且没有一个全局的视图来统一管理这些功能。 一般情况下,网关可以为我们提供请求转发、安全认证(身份/权限认证)、流量控制、负载均衡、降级熔断、日志、监控等功能。 网关主要做了一件事情:请求过滤 。 常见网关 Netflix Zuul Zuul 是 Netflix 开发的一款提供动态路由、监控、弹性、安全的网关服务。 Zuul 主要通过过滤器(类似于 AOP)来过滤请求,从而实现网关必备的各种功能。 ———————————————— 原文链接:https://blog.csdn.net/qq_41244407/article/details/126568832
-
一、微服务演变1、单体架构(Monolithic Architecture)是一种传统的软件架构模式,应用程序的所有功能和组件都集中在一个单一的应用中。在单体架构中,应用程序通常由一个大型的、单一的代码库组成,其中包含了所有的功能模块和业务逻辑。这个应用程序作为一个整体部署和运行在一个应用服务器上,并共享相同的内存和数据库。当单体架构项目的性能无法满足需求时,但又希望继续沿用单体架构的话,你可以采取以下一些优化手段来改善性能,如以下方法:使用缓存:引入缓存机制,将经常请求的数据缓存起来,减少对数据库等后端系统的访问,以提高性能。数据库优化:进行数据库性能调优,包括建立索引、优化查询语句、使用合适的数据库引擎等,以提高数据库的响应速度。使用消息队列:引入消息队列来解耦耗时操作,将其转化为异步的处理任务。这样可以提高并发处理能力和系统的响应性能。水平扩展:通过复制多个实例来处理更多的请求。可以使用负载均衡器将请求分发到不同的实例上,从而提高系统的整体性能和容量。引入分布式架构:将应用程序拆分为多个独立的服务,并通过网络进行通信和协作。这样可以将负载分散到多个节点上,提高整体性能和可伸缩性。接下来就说一下单体架构优化中的引入分布式架构。2、分布式架构将系统的各个组件部署在不同的计算机节点上,并通过网络进行通信和协作的软件架构模式。在分布式架构中,各个节点可以独立运行和处理任务,并通过消息传递、远程过程调用或其他通信机制进行数据交换和协调。在使用分布式时,我们就会有一下的疑问:服务拆分到什么程度?服务集群地址如何维护?服务之间如何实现远程调用?服务健康状况如何感知?3、微服务微服务是一种经过改良好的架构设计的分布式架构方案,微服务架构特征:单一职责:微服务拆分力度更小,每个服务都对应唯一的服务能力,做的单一职责,避免重复开发面向服务:微服务对外暴露业务接口自治:团队独立、技术独立、数据独立、部署独立隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题假如我们有一个电商系统,其中有以下一些微服务:用户服务(User Service)负责用户的注册、登录、信息管理等功能。商品服务(Product Service)负责商品的查询、添加、更新等功能。订单服务(Order Service)负责订单的创建、支付、取消等功能。现在假设用户服务依赖于商品服务来获取商品信息,并且订单服务依赖于用户服务来获取用户信息。这里我们可以看到微服务的几个特征是如何应用的:单一职责:每个微服务都具有清晰的职责。例如,用户服务只负责用户相关的功能,而不涉及商品或订单。面向服务:每个微服务都对外暴露业务接口,其他微服务可以通过调用这些接口来访问所需的功能。例如,用户服务可以提供获取用户信息的接口给订单服务使用。自治:每个微服务的团队在技术和数据上都是独立的。例如,用户服务的团队可以独立开发、测试、部署和扩展该服务,无需依赖其他团队。隔离性强:微服务之间的调用需要做好隔离、容错和降级,以避免出现级联问题。例如,当商品服务不可用时,用户服务可以使用缓存或默认数据来避免影响用户操作。这个例子中提供了不同的微服务来处理不同的功能,并且彼此解耦、独立运行。每个微服务都具有单一职责,对外提供明确定义的业务接口,团队在技术和数据上具有自治能力,同时采取适当的隔离措施来保证系统的弹性和稳定性。这些特征有助于提高开发效率、灵活性和可维护性,使得微服务架构在构建大型、复杂系统时具有优势。4、 总结单体架构特点:简单方便,高度耦合,扩展性差,合适小型项目。如:学生管理系统。分布式架构特点:松耦合,扩展性好,但架构复杂,难道大,适合大型互联网项目。如:京东、淘宝微服务:一种良好的分布式架构方案。优点:拆分粒度更小、服务更独立、耦合更低缺点:架构非常复杂,运维、监控、部署难道更高5、微服务架构微服务这方案需要技术框架来落地实现,全球的换联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是SpringCloud和阿里巴巴的Dubbo。5.1、 微服务技术对比Dubbo、Spring Cloud、Spring Cloud和Spring Cloud Alibaba都是用于构建分布式系统的开源框架。尽管它们的目标相同,但它们在实现方式和功能特点上有所不同。Dubbo:Dubbo是一个高性能的分布式服务框架,由阿里巴巴开发。它基于传统的服务治理理念,提供了服务注册、发现、路由、负载均衡、容错等功能。Dubbo的核心特点是高性能和低延迟的RPC调用,适用于大规模的微服务架构。Dubbo提供了对多种协议(如Dubbo协议、REST协议)和注册中心(如ZooKeeper、Consul)的支持。Spring Cloud:Spring Cloud是一个由Pivotal开发的微服务框架,构建在Spring Framework之上,使得构建分布式系统更加便捷。它提供了一系列的组件和模块,用于实现服务注册与发现、负载均衡、断路器、配置管理、消息总线等功能。Spring Cloud采用了Spring Boot作为底层的开发框架,提供了更简洁、快速搭建分布式系统的解决方案。Spring Cloud Alibaba:Spring Cloud Alibaba是Spring Cloud与Alibaba开放平台合作的结果,提供了一些在云原生应用开发中常用的解决方案。它主要基于Spring Cloud框架,结合了一些Alibaba技术栈,如Nacos(服务注册与发现)、Sentinel(流量控制和熔断降级)、RocketMQ(消息驱动)等。Spring Cloud Alibaba旨在提供云原生应用开发的全栈解决方案。虽然Dubbo和Spring Cloud都是用于构建分布式系统的框架,但Dubbo更加注重于高性能的RPC调用和服务治理,而Spring Cloud则提供了一整套更全面的微服务解决方案。而Spring Cloud Alibaba则是在Spring Cloud的基础上,进一步整合了Alibaba的一些技术,为云原生应用提供更全面的开发支持。选择适合的框架取决于具体的需求、技术栈和团队偏好。5.2、企业需求Spring Cloud + Spring Cloud Alibaba:Spring Cloud提供了丰富的微服务组件和解决方案,包括服务注册与发现、负载均衡、断路器、配置管理等。Spring Cloud Alibaba扩展了Spring Cloud,整合了阿里巴巴技术栈,如Nacos(服务注册与发现)、Sentinel(流量控制和熔断降级)、RocketMQ(消息驱动)等。组合使用这两个框架可以获得全面的云原生应用开发解决方案,适用于构建现代化的微服务架构。Dubbo原生模式 + Spring Cloud Alibaba:Dubbo是一个高性能的RPC框架,提供了服务治理、负载均衡、容错等功能。Spring Cloud Alibaba扩展了Dubbo,为Dubbo提供了更多云原生的支持,如Nacos作为注册中心、Sentinel用于流量控制和熔断降级等。通过将Dubbo和Spring Cloud Alibaba集成,可以获得高性能的RPC调用和全面的云原生的服务治理解决方案。无论是使用Spring Cloud + Spring Cloud Alibaba还是Dubbo原生模式 + Spring Cloud Alibaba,都可以受益于Spring Cloud和Spring Cloud Alibaba提供的丰富的微服务功能和云原生支持。具体选择哪种组合取决于企业需求、技术栈和团队实际情况。需要评估技术要求、性能需求、开发复杂度等因素,选择适合的框架组合来构建稳定、高效的分布式系统。二、spring cloudSpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloudSpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验:springCloud与SpringBoot的版本兼容关系本文学习版本是Hoxton.SR10,因此对应的springboot版本是2.3.x版本。1、服务拆分及远程调用1.1、服务拆分1.1.1、服务拆分注意事项不同微服务,不要重复开发相同业务微服务数据独立,不要访问其他微服务的数据库微服务可以将自己的业务暴露为接口,供其它微服务调用1.1.2、项目实战实战代码:阿里云下载把数据库文件那到数据库管理工具里执行和idea中导入cloud-demo项目最终数据库结构:对与cloud-demo这个项目主要看一下用户和订单如何进行项目拆分的,结合这单体架构的结果对比一下有什么变化和不同。1.1.3、总结微服务需要根据业务模块拆分,做到单一职责,不要重复开发相同业务。微服务可以将业务暴露为接口,供其他微服务使用。不同微服务都应该有自己独立的数据库。1.2、远程调用需求:根据订单id查询订单的同时,把订单所属的用户信息一起返回。1.2.1、远程调用方法在我们的controller类中使用@GetMapping("/user/{id}")向外暴露了一个接口来访问,可以将user信息放回为json数据格式,我们可以想一下浏览器可以发一个Ajax请求来获取数据,我们的订单服务可不可以发一个Ajax请求来获取数据呢?1.2.2、实现远程调用步骤1)注册RestTemplateSpring Cloud中也使用了RestTemplate类。RestTemplate是Spring框架中的一部分,它在Spring Cloud项目中被广泛用于进行微服务之间的通信。在微服务架构中,各个微服务之间通常通过RESTful API进行通信。为了简化这个过程,Spring Cloud对RestTemplate进行了增强,以便更好地支持微服务架构。在Spring Cloud中,RestTemplate被称为"服务调用"的一部分。通过使用RestTemplate,开发人员可以方便地发起HTTP请求来调用其他微服务的API。Spring Cloud还提供了一些增强功能,例如服务发现和负载均衡。开发人员可以使用服务名代替具体的URL,Spring Cloud会自动根据服务名找到可用的实例并进行负载均衡,从而实现更灵活和高效的服务调用。需要注意的是,自Spring Cloud 2020.0.0版本(即Hoxton.SR9及之后的版本)开始,官方推荐使用WebClient替代RestTemplate作为HTTP客户端,因为WebClient提供了更强大、更灵活的功能,并且更适用于非阻塞的响应式编程模型。但是,为了向后兼容,RestTemplate仍然可以继续使用并被支持。在order-service的OrderApplication中注册ResteTemplate,将ResteTemplate用@Bean注解注册为spring管理的对象,以后不管在什么地方都可以使用到RestTemplate对象。完成调用主要是在service层中实现,向被调用服务发起一个Rest请求,在需要调用其他服务的服务中使用,实现步骤:1、在服务启动类中注册RestTemplate,如这里是在order的启动类中2、在要调用其他服务的服务中的service层中自动装配RestTemplate对象3、使用RestTemplate的api来实现即可,里面有很多api,这里我使用的是getForObject()方法4、将远程调用返回的数据封装到要封装的对象中即可,这里我使用的是将远程调用获取的user对象信息封装到order对象中。1.3、提供者与消费者服务提供者:一次业务中,被其他微服务调用的服务。(提供接口给其他服务调用)如:user服务服务消费者:一次业务中,调用其他微服务的服务。(调用其他微服务提供的接口)如order服务总结:1.服务调用关系服务提供者:暴露接口给其它微服务调用服务消费者:调用其它微服务提供的接口提供者与消费者角色其实是相对的一个服务可以同时是服务提供者和服务消费者三、Eureka注册中心1、服务调用出现的问题在前面我们使用RestTemplate来实现服务远程调用,在写url时使用的是硬编码方式,就会产生以下的问题,这些问题是值得我们考虑一下的。服务消费者该如何获取服务提供者的地址信息?如果有多个服务提供者,消费者该如何选择?消费者如何得知服务提供者的健康状态?2、 Eureka基本原理Eureka是Netflix开源的服务治理框架,在Spring Cloud中广泛应用。它的基本原理是建立了一个分布式的服务注册中心,用于管理和维护各个微服务实例的注册和发现。以下是Eureka的基本原理:Eureka服务器:Eureka由一个或多个Eureka服务器组成,它们构成了服务注册中心。每个微服务实例都将自己的信息注册到Eureka服务器,包括服务名、主机名、端口号等。服务注册:微服务启动时,会向Eureka服务器发送注册请求,将自己的信息注册到注册中心。注册中心维护一个服务注册表,记录所有已注册的微服务实例。服务发现:其他微服务需要调用某个服务时,首先向注册中心发送查询请求,获得目标服务的实例列表。注册中心将会返回所有可用的服务实例信息,包括IP地址、端口号等。服务监控:Eureka服务器会定期向已注册的微服务实例发送心跳请求,微服务实例返回响应以证明自己的健康状态。如果一个微服务长时间未发送心跳消息或返回异常状态,Eureka服务器将从注册表中删除该实例。服务同步:Eureka服务器之间会相互复制注册表信息,以保证数据的一致性。当有新的微服务实例注册或注销时,注册中心会通知其他服务器进行注册表更新。通过Eureka提供的服务注册和发现机制,微服务之间可以动态地发现和调用其他微服务,从而实现了服务之间的解耦和灵活性。Eureka还提供了负载均衡、故障恢复等一些附加功能,使得微服务架构更加可靠和高效。回顾之前我们的几个问题:消费者该如何获取服务提供者具体信息?服务提供者启动时向eureka注册自己的信息eureka保存这些信息消费者根据服务名称向eureka拉取提供者信息如果有多个服务提供者,消费者该如何选择?服务消费者利用负载均衡算法,从服务列表中挑选一个消费者如何感知服务提供者健康状态?服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态eureka会更新记录服务列表信息,心跳不正常会被剔除消费者就可以拉取到最新的信息总结在Eureka架构中,微服务角色有两类:1、EurekaServer:服务端,注册中心记录服务信息心跳监控2、EurekaClient: 客户端1)Provider:服务提供者,例如案例中的 user-service注册自己的信息到EurekaServer每隔30秒向EurekaServer发送心跳2)consumer:服务消费者,例如案例中的 order-service根据服务名称从EurekaServer拉取服务列表基于服务列表做负载均衡,选中一个微服务后发起远程调用3、手动实战3.1、搭建eureka服务注册中心(服务名称)1、创建项目,引入spring-cloud-starter-netflix-eureka-server的依赖org.springframework.cloudspring-cloud-starter-netflix-eureka-server2、启用Eureka服务器:创建一个启动类,并使用@EnableEurekaServer注解来启用Eureka服务器功能。@SpringBootApplication@EnableEurekaServerpublic class EurekaApplication {public static void main(String[] args) {SpringApplication.run(EurekaApplication.class,args);}}3、配置Eureka服务器:在应用程序的配置文件(如application.properties或application.yml)中配置Eureka服务器的相关信息,例如:# 服务器配置server:port: 10001 # Eureka服务器运行的端口号# Spring应用程序配置spring:application:name: eurekaserver # Eureka服务器应用程序的名称# Eureka客户端配置eureka:client:service-url:defaultZone: http://localhost:10001/eureka/# Eureka客户端注册自身的Eureka服务器的URL# 在本例中,将Eureka服务器的URL设置为运行在10001端口的本地服务器3.2、注册user-service这个操作是在user-service项目下实现的,主要是将服务信息注册到Eureka服务端,eureka将这些服务信息保存到注册表中进行管理。将user-service服务注册到EurekaServer步骤:1、在user-service项目中引入spring-cloud-starter-netflix-eureka-client的依赖org.springframework.cloudspring-cloud-starter-netflix-eureka-client2、在application.yml位置文件中编写配置spring:application:name: userservice #配置服务名称eureka:client:service-url:defaultZone: http://localhost:10001/eureka/ #配置eureka服务地址信息其实orderservice的注册也是这两个步骤,我就写了。做完之后,我我们可以浏览一下eureka的服务网站看一下:http://localhost:10001这里的端口号是你在自己的eureka的配置文件application.yml配置的eureka端口号一致。总结:1.服务注册引入eureka-client依赖在application.yml中配置eureka地址2.无论是消费者还是提供者,引入eureka-client依赖知道eureka(服务注册中心)地址后,都可以完成服务注册3.3、在order-service完成服务拉取服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡1.修改OrderService的代码,修改访问的url路径,用服务名代替ip、端口:String url ="http://userservice/user/" + order.getUserId();2.在order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解:@Bean@LoadBalanced #负载均衡public RestTemplate restTemplate() {return new RestTemplate();}总结1.搭建EurekaServer引入eureka-server依赖添加@EnableEurekaServer注解在application.yml中配置eureka地址2.服务注册引入eureka-client依赖在application.yml中配置连接eureka服务注册中心地址3.服务发现引入eureka-client依赖在application.yml中配置eureka服务注册中心地址给RestTemplate添加@LoadBalanced注解用服务提供者的服务名称远程调用四、Ribbon负载均衡Ribbon是一个负载均衡解决方案,主要用于在分布式系统中将负载均匀地分发给多个服务实例。它是Netflix开 源的一个组件,常用于微服务架构中。1、负载均衡流程Ribbon的负载均衡原理可以概括如下:服务注册:Ribbon首先需要与服务注册中心(如Eureka、Consul等)进行交互,获取可用的服务实例列表。负载均衡策略:Ribbon支持多种负载均衡策略,如随机策略、轮询策略、权重策略等。根据选择的策略,Ribbon会根据服务实例的状态、性能等因素来选择一个合适的服务实例。服务调用:一旦选择了一个服务实例,Ribbon会将请求发送给该实例。它会维护一个与服务实例的长连接,并在需要时将请求发送给该实例。失败处理:如果请求在与服务实例的通信中失败,Ribbon会尝试选择另一个可用的服务实例进行重试,以增加系统的可用性。Ribbon还可以与其他组件配合使用,例如Netflix的Hystrix熔断器,用于实现服务的容错和故障保护。总之,Ribbon通过动态获取服务实例列表并根据负载均衡策略选择合适的实例来进行负载均衡,从而提高系统的性能、可用性和可扩展性。源码执行流程:2、Ribbon负载均衡策略Ribbon负载均衡规则是一个叫做IRule的接口来实现的,每个子接口都是一种规则:2.1、 负载均衡策略2.2、调整负责均衡策略的规则通过定义IRule实现可以修改负载均衡规则,有两种方式:方式一:代码方式如消费者服务order-service,只要在OrderApplication类中定义一个新的IRule:@Beanpublic IRule iRule(){return new RandomRule();}其实也不一定要在OrderApplication启动类中配置,也可以自己创建一个配置类来配置,在有@configuration注解的类就可以。这种配置方案是全局的配置,只要使用了这种配置方案,以后不管你调用的是user-service服务还是order-service服务都是使用这里的配置方案。方式二:配置文件方式在order-service的application.yml文件中,添加新的配置也可以修改规则:userservice: #被调用微服务器的服务名称ribbon:NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载均衡规则这种配置方案是只针对某个微服务的,是局部配置。3、Ribbon饿加载Ribbon默认是采用懒加载,即第一次访问才会创建LoadBalanceClient,请求时间会很长。而饿加载则会在项目启动时创建,降低每一次访问的耗时3.1、配置开启配置饥饿加载#当服务饥饿加载ribbon:eager-load:enabled: true #开启饥饿加载clients: userservice #指定对userservice这个服务饥饿加载#当有多个服务需要饥饿加载可以用下面的方式:ribbon:eager-load:enabled: true #开启饥饿加载clients: #指定对userservice这个服务饥饿加载- userservice- xxservice总结1.Ribbon负载均衡规则规则接口是IRule默认实现是ZoneAvoidanceRule,根据zone选择服务列表,然后轮询2.负载均衡自定义方式代码方式:配置灵活,但修改时需要重新打包布配置方式:直观,方便,无需重新打包发布但是无法做全局配置3.饥饿加载开启饥饿加载指定饥饿加载的微服务名称五、Nacos注册中心微服务的注册中心我们已经学了eureka,大家肯定会有疑问,为啥还有学校nacos呢?其实nacos不仅有服务注册中心和服务发现功能,还有更加强大的功能。1、nacos和eureka对比Nacos和Eureka是两个常用的服务发现和注册工具,它们都具有类似的功能,但在一些方面存在一些差异。以下是Nacos和Eureka的一些对比:开发者生态圈:Nacos是由Alibaba开发并开源,拥有庞大的Alibaba生态圈支持,而Eureka是由Netflix开发并开源,得到了Netflix和Spring Cloud社区的广泛应用。功能和特性:Nacos提供了更多的功能和特性,除了服务注册与发现外,还包括配置管理、动态DNS、动态路由和流量管理等功能。Eureka主要关注服务注册与发现的功能。容错性:Nacos具有更强的容错性,支持多数据中心的分布式部署,可以保证在网络分区和节点故障情况下的高可用性。Eureka在这方面的容错性相对较弱。数据一致性:Nacos使用Raft算法来实现数据一致性和高可用性,而Eureka使用的是AP模型,即优先保证可用性而不保证强一致性。社区活跃度:Nacos的开源社区活跃度相对较高,有更多的贡献者和更新的版本发布。Eureka的开源社区相对较少活跃,更新较为缓慢。选择使用Nacos还是Eureka可以根据具体需求和项目背景来决定。如果需要更多的功能和特性,以及较强的容错性和高可用性,Nacos可能是更好的选择。如果项目已经依赖于Netflix和Spring Cloud生态圈,或者对于服务注册和发现的简单功能需求,Eureka可能是更适合的选项。2、Nacos下载安装服务注册中心2.1下载nacos在Nacos的GitHub页面,提供有下载链接,可以下载编译好的Nacos服务端或者源代码:GitHub主页:https://github.com/alibaba/nacosGitHub的Release下载页:https://github.com/alibaba/nacos/releases这里我提供了阿里云盘下载:Nacos1.4.12.2、解压Nacos我这里使用的是在Windows下安装的。直接解压到一个没有中文字符的路径下。目录说明:- bin:启动脚本- conf:配置文件2.2.1、配置端口Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程。**如果无法关闭占用8848端口的进程**,也可以进入nacos的conf目录,修改配置文件中的端口:修改其中的内容修改为一个没有被占用的端口号。2.2.2.启动nacos服务注册中心启动非常简单,进入bin目录,进入cmd窗口执行下面的命令startup.cmd -m standalone#startup.cmd -m standalone 的含义是执行Nacos服务器的启动脚本,并使用独立模式启动Nacos服务器。#这将在单个节点上运行Nacos,并使用默认的配置文件和端口。这个命令适用于简单部署或测试,并非生产环境下常见的集群模式启动命令。执行后的效果如图:在浏览器输入地址:http://127.0.0.1:8848/nacos即可:用户名和密码都是:nacos网页直接就是中文的,对于英语不太好的比eureka的友好。3、将服务注册到nacos服务注册中心1、在cloud-demo父工程中添加spring-cloud-alibaba的管理依赖:com.alibaba.cloudspring-cloud-alibaba-dependencies2.2.5.RELEASEpomimport2、注释掉order-service和user-service中原有的eureka依赖。3、添加nacos的客户端依赖:com.alibaba.cloudspring-cloud-starter-alibaba-nacos-discovery4、修改user-service & order-service中的application.yml文件,注释掉eureka的地址,添加nacos地址:spring:cloud:nacos:server-addr: localhost:8848启动项目测试:其实eureka和nacos在spring-cloud中除了依赖和端口号配置不同外其他的使用都是一样的,他们底层使用的接口都是一样的,所以代码其他地方根本不用做改变。总结:1.Nacos服务搭建下载安装包解压在bin目录下运行指令:startup.cmd -mstandalone2.Nacos服务注册或发现引入nacos.discovery依赖配置nacos地址spring.cloudnacos.server-addr4、Nacos服务分级存储模型之前我们在学中,我们的服务分层都是只有两层的,第一层是服务,第二层是实例(如user-service、order-service),在我们部署时,都是将这些服务我实例部署到一个机器上或者一个机房里,但是如果这个机房收到破坏(地震、火灾)时,服务就无法访问了。而nacos服务分层存储模型则引出了集群的概念,一个集群就好比是我们的一个机房,而我们的一个服务下面就可以部署很多个集群,每个集群就可以部署在全球各地(北京、上海、深圳等),因此服务的层次结构就变成了三层(服务、集群、实例)。4.1、服务跨集群调用问题服务调用尽可能选择本地集群的服务,跨集群调用延迟高本地集群不可以访问时,在去访问其他集群4.2、服务集群实现1、修改application.yml,添加如下内容,配置服务集群名称spring:cloud:nacos:server-addr: localhost:8848 #nacos 服务端地址cluster-name: YN #配置集群名称,也就是机房位置,例如:YN ,云南2、在Nacos控制台可以看到集群变化现在的集群变成了yn,在没有配置集群名称是default。总结1.Nacos服务分级存储模型一级是服务,例如userservice二级是集群,例如杭州或上海三级是实例,例如杭州机房的某台部署了userservice的服务器2.如何设置实例的集群属性修改application.yml文件,添加spring.cloud.nacos.discovery.cluster-name属性即可4.3、Nacos集群负载均衡Nacos集群负载均衡的规则其实是优先选择本集群内的服务,如果本服务挂了,才会去选择其他集群的服务来访问。配置1、修改order-service中的application.yml,设置集群为HZ:spring:cloud:nacos:server-addr: Localhost:8848 # nacos 服务端地加discovery:cLuster-name: HZdiscovery:cluster-name: bj #配集群名称,也就是机房位置2、然后在order-service中设置负载均衡的IRule为NacosRule,这个规则优先会寻找与自己同集群的服务:userservice: #服务名称ribbon:NFLoadBalancerRulecassName: com.alibaba.cloud.nacos.ribbon.NacosRule #负载均衡规则3、注意将user-service的权重都设置为1总结:1.NacosRule负载均衡策略优先选择同集群服务实例列表本地集群找不到提供者,才去其它集群寻找,并且会报警告确定了可用实例列表后,再采用随机负载均衡挑选实例4.4、根据权重负载均衡假设有一个微服务架构的电子商务网站,其中包括以下几个服务:商品服务、订单服务和用户服务。这些服务都注册到了Nacos中进行服务发现,并且它们有不同的实例数和性能配置。在这种情况下,可以通过基于权重的负载均衡来实现对这些服务实例的合理请求分配。以下是一些示例场景:不同实例性能差异较大:假设商品服务有3个实例,其中实例A和实例B配置较高,实例C配置较低。此时,可以设置实例A和实例B的权重为2,实例C的权重为1。这样,每次有请求到来时,有较高性能的实例A和实例B将会处理更多的请求,而较低性能的实例C将会处理较少的请求,从而实现了性能调控。服务实例容量不均衡:假设订单服务有5个实例,其中实例A、B和C的容量比实例D和E大。可以为实例A、B和C设置较高的权重值,如3,而为实例D和E设置较低的权重值,如1。这样,请求将均衡分配到实例A、B和C之间,并保持实例D和E的请求数较少。降低故障实例的负载:假设用户服务有4个实例,其中实例A由于某种原因,出现了故障或不稳定情况。为了减少对实例A的请求分配,可以为其设置较低的权重值,如0,而将其他正常的实例设置为较高的权重值,如3。这样,请求将主要被分配给其他正常的实例,降低了对故障实例A的负载。通过在Nacos中设置服务实例的权重,可以根据实际情况动态调整请求的负载比例。这样能够充分利用资源、提高系统性能、保证服务稳定性,并对不同实例进行合理分配。Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。配置4.5、环境隔离-namespace1、认识环境隔离-namvespace应用场景:假设有一个电商平台,该平台设计了一个基于Nacos的命名空间配置和服务注册方案,以支持环境隔离和多租户功能。首先,假设该电商平台有三个环境:开发环境、测试环境和生产环境。每个环境都有独立的配置和服务需求。环境隔离和配置管理:使用Nacos的命名空间功能,平台管理员可以创建三个命名空间:dev-namespace、test-namespace和prod-namespace。在dev-namespace中,可以配置开发环境所需的各种参数,如数据库连接信息、调试模式等。在test-namespace中,可以配置测试环境所需的参数,如测试数据库连接信息、测试数据源等。在prod-namespace中,可以配置生产环境所需的参数,如正式数据库连接信息、生产级别的服务配置等。每个命名空间下的配置项是相互隔离的,这样就保证了不同环境配置的独立性,并且可根据需求灵活管理和更新配置信息。服务注册和版本管理:在每个命名空间中,可以注册相应环境需要的服务实例,如商品服务、订单服务等。平台管理员可以使用命名空间功能来管理不同环境的服务注册表,确保每个环境只能访问属于自己的服务实例。每个命名空间可以独立管理服务实例的版本,例如,在开发环境中可以注册和测试新的服务版本,而在生产环境中则使用稳定的服务版本。通过这样的命名空间配置和服务注册方案,电商平台能够实现对不同环境的配置和服务进行隔离,并支持多租户功能。开发团队可以独立管理各自环境的配置和服务,而不会相互干扰。此外,平台还可以利用灰度发布和版本管理功能,在不同命名空间中进行服务版本控制,确保系统稳定性和可用性。2、配置命名空间1)创建命名空间(在nacos服务页面创建)2、将服务配置到指定的命名空间中(修改application.yml配置文件)server:port: 8081spring:datasource:url: jdbc:mysql://192.168.10.130:3306/cloud-user?useSSL=falseusername: rootpassword: rootdriver-class-name: com.mysql.jdbc.Driverapplication:name: userservice #配置服务名称cloud:nacos:server-addr: localhost:8848discovery:cluster-name: yn #集群名称,地址在云南namespace: 074da7cb-c3e3-4848-b893-3d15114e8729 #命令空间id此时,配置了namespace的服务已经被配置到dev中了如果此时使用order-service去调用user-service就会报错了Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalStateException: No instances available for userservice] with root causeservlet .service()的servlet [dispatcherServlet]在上下文与路径[]抛出异常[请求处理失败;嵌套异常是java.lang.IllegalStateException: No instances available for userservice],有根本原因5、nacos注册中介细节分析服务注册:服务提供者启动时,会向服务注册中心发起注册请求,包括服务名、IP 地址、端口等。注册中心会将这些信息保存起来。服务发现:服务发现有两种(pull和push相结合)pull:服务消费者会每隔30秒定时主动向服务注册中心发起请求,来拉取服务信息,在服务消费者中有一个服务列表缓存,用于缓存从服务注册中心拉取下的服务信息。push:当有服务停掉后服务注册中心会主动推送变更信息给消费者,消费者就会更新服务列表缓存。心跳与健康检查:也是有两种(对临时实力和非临时实例)临时实例:采用心跳检测,是临时实例主动向注册中心发送心跳检测,如果一段时间注册中心没有收到临时实例心跳检测,就会剔除该临时实例。非临时实例:采用nacos主动询问,nacos主动询问非临时实例的健康状态,如果非临时实例停止了,也不会剔除非临时实例,只是修改非临时实例的健康转态,会等待非临时实例健康。负载均衡:负载均衡和流量控制是由服务消费者一侧的客户端组件来实现的,而不是由注册中心来处理。当客户端从Nacos注册中心获取到可用的服务实例列表后,负载均衡和流量控制的责任落在了客户端的实现上。5.1、临时实例与非临时实例Nacos中的实例分为临时实例和非临时实例,它们在生命周期和用途上有所不同:1、非临时实例(Persistent Instance):非临时实例是指注册到Nacos注册中心的服务实例,其生命周期不会受到外部因素的影响,除非主动取消注册或服务下线。这种实例适用于通常情况下稳定运行的服务,它们的注册信息会被持久化存储在Nacos服务器中,提供持久性的服务发现和注册功能。2、临时实例(Ephemeral Instance):临时实例是指服务实例在持有连接的客户端断开连接时,会自动从Nacos注册中心上注销的一种实例类型。当客户端与Nacos注册中心建立心跳连接后,临时实例会周期性地向注册中心进行连接进行检测。如果一段时间注册中心没有收到临时实例的心态检测,注册中心会将对应的实例注销。临时实例适用于动态扩缩容、临时性访问等场景,允许实例根据连接状态进行动态管理。临时实例和非临时实例在实践中有不同的应用场景:对于稳定运行的服务,应使用非临时实例。这样服务实例的注册信息将持久存在于注册中心中,即使服务发生故障或重启,也能保证注册信息的持久性,其他服务能够继续发现和使用该实例。对于临时性的任务,如定时任务、临时数据处理服务等,可以选择使用临时实例。这样在任务完成后,实例会自动注销,降低注册中心中无用实例的数量,也能更好地适应动态需求变化。需要注意的是,临时实例的自动注销是基于与Nacos注册中心之间的心跳连接,而非基于服务实例的具体运行状态。因此,在使用临时实例时,需要相应地配置和管理心跳连接,以保证实例的不间断注册和注销。5.2、配置临时实例与非临时实例(默认是临时实例)服务注册到nacos时,可以选择注册为临时或非临时实例,通过在application.yml文件中添加下面的配置来设置:spring:cloud:nacos:discovery: #discovery:发现,透露 ; ephemeral:短暂的ephemeral: false #设置为非临时实例总结1.Nacos与eureka的共同点都支持服务注册和服务拉取都支持服务提供者心跳方式做健康检测2.Nacos与Eureka的区别Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式临时实例心跳不正常会被剔除,非临时实例则不会被剔除Nacos支持服务列表变更的消息推送模式,服务列表更新更及时Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式6、Nacos配置管理以下是几个使用Nacos配置管理的实际业务场景的例子:微服务配置管理:假设你的公司采用了微服务架构,有多个微服务需要连接到不同的数据库。通过使用Nacos配置管理,你可以将每个微服务的数据库连接信息存储在Nacos中,并动态地更新配置。例如,当你需要更改数据库的连接地址或密码时,你可以直接在Nacos中修改配置,而无需重新部署微服务,所有微服务将自动获取最新的配置信息。多环境配置管理:假设你的应用在开发、测试和生产环境中运行,并且每个环境都有不同的配置。使用Nacos配置管理,你可以为每个环境创建不同的配置文件,并通过Nacos的命名空间和配置组进行组织。开发人员可以通过选择不同的命名空间和配置组,轻松地切换到不同的环境,而不必手动修改配置文件。动态路由配置:假设你的微服务架构中使用了Spring Cloud Gateway作为API网关,并且你希望能够动态路由请求到不同的后端服务。通过使用Nacos配置管理,你可以将路由规则存储在Nacos中,并通过Spring Cloud Gateway与Nacos集成。当需要更新路由规则时,你可以直接在Nacos中修改配置,Spring Cloud Gateway将自动更新路由,并将请求动态地转发到相应的后端服务。定时任务配置:假设你的应用需要执行定时任务,例如生成报表或清理数据。通过使用Nacos配置管理,你可以将定时任务的触发时间、任务参数等信息存储在Nacos中,并由任务调度器定期从Nacos获取最新的配置。当你需要调整定时任务的执行时间或参数时,只需在Nacos中修改配置,任务调度器将自动根据最新的配置执行任务。这些例子只是Nacos配置管理功能的一部分,Nacos还提供了更多的特性,例如配置监听、配置推送等,可以根据具体的业务需求进行灵活使用。6.1、统一配置管理6.1.1、配置更改热更新在Nacos中添加配置信息:在弹出表单中填写配置信息:在nacos服务页面中配置,让服务找到nacos的配置:1、引入nacos的配置管理客户依赖:com.alibaba.cloudspring-cloud-starter-alibaba-nacos-config2、在userservice中的resource目录添加一个bootstrap.yml文件,这个文件是引导文件,优先级高于application.yml:spring:application:name: userservice #配置服务名称cloud:nacos:server-addr: localhost:8848 #配置nacos服务地址config:file-extension: yaml #文件后缀profiles:active: dev #开发环境,这里是dev可以在Usercontroller类中获取一下nacos配置的信息来验证一下:@Value("${pattern.dateformat}")private String dateformat;@GetMapping("/now")public String now(){return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));}6.1.2、配置自动刷新nacos中的配置文件变更后,微服务无需重新启动就可以感知。不过需要通过下面两种配置实现:方式一:在@Value注入的变量所在类上添加注解@RefreshScope方式二:使用@ConfigurationProperties注解这种方式使用的是约束大于配置的配置方法。@Data@Component@ConfigurationProperties(prefix = "pattern")public class PatternProperties {private String dateformat;}使用了Spring框架的注解(Data,Component,ConfigurationProperties)。这段代码定义了一个名为PatternProperties的类。@ConfigurationProperties注解是Spring框架中用于绑定配置属性的注解。它的prefix属性指定了配置文件中的属性前缀为"pattern",这意味着在配置文件中,所有以"pattern"开头的属性会被绑定到PatternProperties类的对应属性上。PatternProperties类中只有一个私有的String类型属性dateformat,它对应配置文件中的"pattern.dateformat"属性。在配置文件中,你可以设置这个属性的值,例如:pattern:dateformat: yyyy-MM-dd HH:mm:ss这样,当你运行程序时,Spring框架会自动将配置文件中的值绑定到PatternProperties类的dateformat属性上,你可以通过获取PatternProperties实例的方式来访问并使用这个属性的值。在类中使用nacos中配置时可以通过以下方式获取:总结Nacos配置更改后,微服务可以实现热更新,方式:通过@Value注解注入,结合@RefreshScope来刷新通过@ConfigurationProperties注入,自动刷新注意事项:不是所有的配置都适合放到配置中心,维护起来比较麻烦建议将一些关键参数,需要运行时调整的参数放到nacos配置中心,一般都是自定义配置7、Nacos集群搭建7.1、集群结构图官方给出的Nacos集群图:其中包含3个nacos节点,然后一个负载均衡器代理3个Nacos。这里负载均衡器可以使用nginx。DNS(Domain Name System)、SLB(Server Load Balancer)和Nacos可以一起使用来构建一个完整的服务发现和负载均衡的系统。DNS是用于将域名解析为对应的IP地址的系统。在服务发现和负载均衡中,DNS可以被用来解析服务名称为对应的服务实例的IP地址。例如,假设有一个服务名为"my-service",通过将其注册到DNS中,可以将"my-service"解析为具体的服务实例的IP地址。SLB是一种负载均衡器,用于将流量分发到多个后端服务实例上,从而实现负载均衡和高可用性。SLB可以接收客户端请求,并根据负载均衡算法将请求分发到多个服务实例上。例如,当有多个服务实例提供相同的服务时,SLB可以根据负载情况,将请求分发到负载较低的实例上,以实现流量的均衡分布。Nacos是一个用于服务发现、配置管理和服务治理的开源项目。在服务发现和负载均衡中,Nacos作为服务注册中心和配置中心,可以用来管理各个服务实例的注册和注销,以及维护服务实例的元数据信息。Nacos可以提供给SLB和DNS所需的服务实例信息,从而实现服务发现和负载均衡的功能。结合这三个组件,整体的架构可以如下所示:服务实例注册:服务实例可以将自己的元数据信息注册到Nacos中,包括服务名称、IP地址、端口等信息。Nacos服务注册中心:Nacos注册中心负责存储和管理所有服务实例的元数据信息。Nacos配置中心:Nacos配置中心负责存储和管理应用程序的配置信息。DNS解析:客户端可以通过DNS解析服务名称为具体的服务实例的IP地址。SLB负载均衡器:SLB可以接收客户端请求,并根据负载均衡算法将请求分发到多个服务实例上。通过这样的结构,客户端可以通过DNS解析获取到服务实例的IP地址,并通过SLB将请求发送到可用的服务实例上,实现负载均衡。同时,Nacos作为服务注册中心和配置中心,可以管理服务实例的注册和配置信息,以确保服务的可用性和配置的一致性。这样的架构既提供了可靠的服务发现和负载均衡,也保证了配置的集中管理和动态更新能力。我们计划的集群结构:三个nacos节点的地址:| 节点 | ip | port || nacos1 | 192.168.150.1 | 8845 || nacos2 | 192.168.150.1 | 8846 || nacos3 | 192.168.150.1 | 8847 |7.2、集群搭建搭建集群的基本步骤:- 搭建数据库,初始化数据库表结构- 下载nacos安装包- 配置nacos- 启动nacos集群- nginx反向代理六、Feign远程调用RestTemplate方法调用存在的问题先看一下我们以前利用RestTemplate发起远程调用的代码:存在下面的问题:代码可读性差,编程体验不统一参数复杂URL难维护1、Feign的介绍Feign 提供了一种简单且优雅的方式来定义和调用基于HTTP的远程服务。通过使用Feign,您可以在客户端代码中定义接口,然后Feign会根据这些接口的定义自动生成实际的HTTP请求,并将其转发到远程服务。这样,您可以像调用本地方法一样调用远程服务的方法,无需显式地处理HTTP请求和响应。Feign 还支持对请求进行编码和解码、错误处理、请求和响应拦截器等功能,使得在微服务架构中处理远程服务变得更加方便和高效。使用Feign的一些优点包括:简化了客户端代码,使其更易于维护和理解。减少了手动处理HTTP请求和响应的工作量。支持多种编解码器,可处理多种数据格式。可与Spring Cloud等微服务框架无缝集成。在使用Feign时,您需要定义一个Java接口,该接口包含与远程服务相对应的方法和参数。然后,通过在应用程序的配置中启用Feign并使用Spring的依赖注入功能,您可以将Feign客户端注入到您的代码中,从而实现对远程服务的调用。Feign作为一个声明式的HTTP客户端,在微服务架构和分布式系统中有许多应用场景。以下是一些常见的使用场景:微服务间的通信:在微服务架构中,各个服务之间需要频繁地进行通信,Feign可以帮助简化服务间的HTTP通信,使得调用远程服务更加方便。服务消费者:当一个服务需要调用其他服务提供的API时,可以使用Feign来作为客户端来消费这些服务,而无需手动处理HTTP请求和响应。代理远程API:Feign可以将远程服务的API映射为本地接口,使得调用远程服务的过程就像调用本地方法一样简单。负载均衡:结合负载均衡的工具(如Ribbon),Feign可以实现在多个服务实例之间进行负载均衡,从而提高系统的可用性和性能。声明式的错误处理:Feign支持定义统一的错误处理逻辑,使得在发生错误时可以采取一致的处理方式,从而减少重复代码。数据格式处理:Feign支持多种编解码器,可以处理不同的数据格式,例如JSON、XML等,使得数据的传输和解析更加灵活和便捷。请求拦截与日志:Feign支持请求和响应拦截器,可以在发送请求和接收响应时进行拦截和处理,例如记录日志、鉴权等。总体而言,Feign适用于任何需要在微服务架构中进行HTTP通信的场景,特别是当您希望简化远程服务调用的代码并增加可读性和可维护性时,Feign是一个非常有用的工具。2、定义和使用Feign客户端在之前我是在调用其他服务提供的接口是使用的是RestTempale,为什么还要学Feign呢?Feign和RestTemplate都是在Spring框架中用于进行HTTP请求的工具,但它们在使用方式和特点上有一些区别。声明式 vs. 编程式:Feign是一个声明式的HTTP客户端,它允许您通过定义接口来描述对远程服务的请求,并自动生成底层HTTP调用。Feign使用注解来配置请求的URL、HTTP方法、请求参数等信息,使得代码更加简洁和易读。RestTemplate是一个编程式的HTTP客户端,您需要在代码中显式地构建HTTP请求,包括指定URL、HTTP方法、请求头、请求体等信息。虽然可以通过RestTemplate灵活地控制请求细节,但相比Feign,代码可能会更冗长和复杂。整合Spring Cloud vs. 单独使用:Feign是Spring Cloud项目的一部分,它与Spring Cloud的其他组件(如Eureka、Ribbon、Hystrix等)紧密集成,使得在微服务架构中使用Feign更加方便,并且提供了一些额外的特性,如负载均衡、服务发现等。RestTemplate是Spring Framework的一部分,它可以单独使用,没有与Spring Cloud的深度集成。如果您在非微服务环境中,或者不需要Spring Cloud提供的其他功能,RestTemplate是一个不错的选择。自动化的负载均衡:Feign与Ribbon(Spring Cloud中的负载均衡组件)集成,可以自动进行负载均衡,使得在多个服务实例中选择合适的目标服务。RestTemplate在默认情况下不支持自动的负载均衡,您需要手动编写代码来实现负载均衡,或者结合Ribbon来实现自动化负载均衡。请求拦截器和错误处理:Feign允许您定义请求和响应拦截器,从而可以在发送请求和接收响应时进行拦截和处理,例如记录日志、鉴权等。它还提供了声明式的错误处理机制,让您可以统一处理请求错误。RestTemplate也支持请求和响应拦截器,但是在处理错误时可能相对繁琐,需要通过捕获异常等方式来处理请求错误。综上所述,如果您在使用Spring Cloud和微服务架构,特别是在Feign与Ribbon、Eureka等组件进行集成时,Feign可能是更好的选择,因为它提供了一种简单且声明式的方式来定义和调用远程服务。然而,如果您在非微服务环境或不需要Spring Cloud提供的其他功能,RestTemplate仍然是一个可行的选择,尤其是当您需要更多的灵活性和对HTTP请求的直接控制时。2.1、Feign实战定义和使用Feign客户端需要以下步骤:1、添加依赖:首先,您需要在项目中添加Feign的依赖。如果您是使用Spring Boot项目,可以在pom.xml中添加以下依赖:org.springframework.cloudspring-cloud-starter-openfeign2、启用Feign客户端:为了使Feign客户端生效,您需要在Spring Boot应用程序的主类上添加@EnableFeignClients注解,这将启用Feign客户端的自动配置和发现。@MapperScan("cn.itcast.order.mapper")@SpringBootApplication@EnableFeignClientspublic class OrderApplication {public static void main(String[] args) {SpringApplication.run(OrderApplication.class, args);}}3、创建Feign客户端接口:接下来,您需要定义一个Java接口,该接口将包含与远程服务相对应的方法和参数。这些方法的定义类似于普通的Spring组件接口,但是您可以使用Spring的注解来定义远程服务的URL、HTTP方法和其他相关信息。package cn.itcast.order.clients;import cn.itcast.order.pojo.User;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;@FeignClient("userservice") //使用@FeignClient来指导这个接口所有接口方法要访问的服务名称public interface UserClient {//定义调用接口@GetMapping("/user/{id}")User findUserById(@PathVariable("id") Long id);}4、使用Feign客户端:现在您可以在其他组件或服务中注入Feign客户端,并使用它来调用远程服务的方法。package cn.itcast.order.service;import cn.itcast.order.clients.UserClient;import cn.itcast.order.mapper.OrderMapper;import cn.itcast.order.pojo.Order;import cn.itcast.order.pojo.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate UserClient userClient; //注入User的Feign客户端public Order queryOrderById(Long id){Order order = orderMapper.findById(id);Long userId = order.getUserId();//使用Feign远程调用User user = userClient.findUserById(userId);order.setUser(user);return order;}}总结Feign的使用步骤引入依赖添加@EnableFeignClients注解编写FeignClient接口使用FeignClient中定义的方法代替RestTemplate3、自定义Feign的配置Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:一般我们需要配置的就是日志级别。日志级别Feign支持四种不同的日志级别,您可以根据需要选择适合的日志级别。这些日志级别用于控制Feign在发送请求和接收响应时记录的日志信息的详细程度。NONE:该日志级别最低,不记录任何日志信息。如果您不想在控制台输出任何关于Feign请求和响应的日志,可以选择此级别。BASIC:在BASIC级别下,Feign仅记录请求方法、URL和响应状态码的基本信息。这对于快速了解请求的基本情况很有帮助,但不会记录请求和响应的详细内容。HEADERS:在HEADERS级别下,Feign将记录请求和响应的头部信息,包括请求头和响应头。这样可以更详细地查看请求和响应的头部信息,有助于调试和了解请求的上下文。FULL:FULL级别是最详细的日志级别,它会记录请求和响应的所有详细信息,包括请求头、请求体、响应头和响应体。如果您需要完整的请求和响应信息来进行详细的调试和排查问题,FULL级别是最合适的选择。3.1自定义Feign的配置方式方式一:配置文件方式当使用配置文件的方式来配置Feign的自定义配置时,您可以借助Spring Boot的属性配置功能来实现。通过在配置文件(如application.properties或application.yml)中添加特定的属性,您可以自定义Feign的行为。首先,您需要在配置文件中添加Feign的相关属性。以YAML格式的配置文件为例,假设您想要配置Feign的日志级别为FULL,可以这样写:1、全局生效# application.ymlfeign:client:config:default: #这里用default就是全局配置,如果是写服务器名称,则是针对在某个微服务的配置loggerLevel: full #日志级别2、局部生效# application.ymlfeign:client:config:orderservice: #这里用服务名称,则只针对这个服务的配置loggerLevel: full #日志级别方式二:java代码方式,需要先声明一个Bean:在使用Feign时,您可以通过自定义配置来修改其行为和属性。为了自定义Feign的配置,您需要创建一个配置类,并在其中添加相关的Bean定义。下面是一个简单的示例来说明如何自定义Feign的配置:package cn.itcast.order.config;import feign.Logger;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class FeignConfig {@Beanpublic Logger.Level feignLogLevel(){return Logger.Level.BASIC;}}全局配置:如果是全局配置,则把它放到@EnableFeignClients这个注解中:@EnableFeignClients(defaultConfiguration = FeignConfig.class)局部配置:如果是局部配置,则把它放到@FeignClient这个注解中:@FeignClient(value = "userservice",configuration = FeignConfig.class)总结Feign的日志配置1.方式一是配置文件,feign.client.config.xxx.loggerLevelO如果xxx是default则代表全局如果xxx是服务名称,例如userservice则代表某服务2.方式二是java代码配置Logger.Level这个Bean如果在@EnableFeignClients注解声明则代表全局如果在@FeignClient注解中声明则代表某服务4、Feign的性能优化Feign的底层客户端实现是通过集成了其他HTTP客户端库来实现的。具体来说,Feign支持两种主要的HTTP客户端实现:JDK的URLConnection和Apache HttpClient。JDK的URLConnection:这是Java标准库中提供的用于HTTP通信的API。Feign可以直接使用JDK的URLConnection来发送HTTP请求。它是轻量级的,对于简单的HTTP通信,可能足够满足需求。如果您在项目中没有引入其他HTTP客户端库,Feign会默认使用JDK的URLConnection作为底层的客户端实现。Apache HttpClient:Apache HttpClient是Apache基金会提供的一个功能丰富、灵活的HTTP客户端库。它提供了许多高级功能,如连接池、重试机制、认证等。如果您在项目中引入了Apache HttpClient的依赖,Feign会自动选择使用Apache HttpClient作为底层的客户端实现。OkHttp:OkHttp是Square公司开发的一款高效的HTTP客户端库。它支持HTTP/2、连接池、拦截器等现代特性。如果您在项目中引入了OkHttp的依赖,Feign会自动切换到OkHttp作为底层实现。Feign的这种设计使得您可以根据需要灵活地选择底层的HTTP客户端。默认情况下,如果项目中没有引入其他HTTP客户端库,Feign将使用JDK的URLConnection作为底层客户端。如果您希望使用Apache HttpClient或OkHttp,只需在项目中添加相应的依赖,Feign会自动检测并使用它们作为底层实现。通过这种方式,Feign可以同时满足不同项目对HTTP客户端的需求,并提供简便的远程服务调用方式。因此优化Feign的性能主要包括:使用连接池代替默认的URLConnection日志级别,做好使用basic或者none4.1、 Feign性能优化HttpClient的支持:1、Feign添加HttpClient的支持:引入依赖:io.github.openfeignfeign-httpclient配置连接池:#feign自定义配置,配置日志级别feign:client:config:default: #这里用default就是全局配置,如果是写服务器名称,则是针对在某个微服务的配置loggerLevel: basic #日志级别httpclient:enabled: true #开启feign对HttpClient的支持max-connections: 200 #最大连接数量max-connections-per-route: 50 #每个路径的最大连接数量总结Feign的优化1.日志级别尽量用basic2.使用HttpClient或OKHttp代替URLConnection引入feign-httpClient依赖配置文件开启httpClient功能,设置连接池参数5、Feign的最佳实际方式一(继承):给消费者的FeignClient和提供者的controller系统定义一个父接口作为标准。这种实现方式虽然可以让Controller和Feign Client共享同一个接口定义,但存在一些问题和注意事项:潜在的耦合:共享同一个接口定义会让Controller和Feign Client在代码层面产生耦合,导致它们紧密地关联在一起。一旦接口定义发生变化,两者都需要进行相应的修改,这可能影响到多个模块。不符合单一职责原则:Controller负责处理HTTP请求和返回响应,而Feign Client负责远程服务调用。将它们共享同一个接口可能会让代码功能变得混乱,违反了单一职责原则。难以做到完全解耦:尽管接口定义可以共享,但是在Feign Client的实现中,仍然需要涉及到远程服务调用的逻辑。这会让Feign Client的实现和Controller之间仍然有一定的耦合。接口定义可能会变得复杂:为了适应不同的调用场景,共享接口可能会变得复杂,可能需要添加各种参数和注解,从而导致接口的冗长和不易维护。功能不一致问题:在Controller和Feign Client共享同一个接口的情况下,两者的功能可能不完全一致。在Controller中可能需要做一些本地逻辑处理,而在Feign Client中可能需要进行额外的远程服务调用。综上所述,虽然共享接口可以在一定程度上减少重复代码,但也会引入潜在的问题和复杂性。在实际开发中,更常见的做法是将Controller和Feign Client分别定义独立的接口,通过接口定义来规范各自的功能和职责。在需要共享方法定义的情况下,可以使用Java接口继承来实现方法的复用,而避免在Feign Client中直接实现Controller的方法。这样可以更好地保持代码的清晰和可维护性,并符合单一职责原则。方式二(抽取):将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用以前实现假如:我们有order-service、pay-service和user-service两个服务,现在order-service和pay-service都要去调用user-service提供的接口,我们就得order-service和pay-service服务里都要实现UserClient,假如我们不止只有这两个服务调用user-service呢,有10个、20个服务都要调用,那UserClient就得写10遍、20遍,就会重复的开发代码了。现在我们独立创建一个项目,将哪些重复的代码编写的东西全部放到这个项目/模块中(如UserClient、User实体类、DefaultConfig等),我们可以把这个项目的内容打成jar包,假如以后服务需要的时候直接引用依赖就可以总结Feign的最佳实践让controller和FeignClient继承同一接口将FeignClient、poJo、Feign的默认配置都定义到一个项目中,供所有消费者使用抽取FeignClient实现最佳实践方式二的步骤如下:1、创建一个module,命名为feign-api,然后引入feign的starter依赖引入feign的starter依赖:org.springframework.cloudspring-cloud-starter-openfeign2、将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中之前在order-service服务模块中饿这些东西就可以删除了。3、在order-service中引入feign-api的依赖cn.itcast.demofeign-api1.0这个依赖是我们项目中的feign-api模块里的内容。4、修改order-service中的所有与上述三个组件有关的import部分,改成导入feign-api中的包5、重启测试当定义的FeignClient不在springApplication的包扫描范围时,这些FeignClient无法使用问题重启order-service服务后我们会发现报错了Field userClient in cn.itcast.order.service.OrderService required a bean of type 'cn.itcast.feign.clients.UserClient' that could not be found.该错误信息表明在OrderService类中的userClient字段的类型为cn.itcast.feign.clients.UserClient,但是Spring容器中找不到该类型的Bean。说明我们在自动装配UserClient时是找不到这个对象的实例的,原因是我们现在在order-service模块中使用的UserClient是通过添加了feign-api依赖而导进来的,在order-service模块启动时spring只会对启动类所在目录及这个目录里所有类进行扫描注册到spring容器中的,而UserClient所在的包就没有被扫描到,所在在order-service这个模块中的spring容器中就找不到了对于的对象实例了。解决办法方式一:指定FeignClient所在包@EnableFeignClients(basePackages = "cn.itcast.feign.clients")解决办法方式二:指定FeignClient字节码@EnableFeignClients(clients = {UserClient.class})总结不同包的FeignClient的导入有两种方式在@EnableFeignClients注解中添加basePackages,指定FeignClient所在的包在@EnableFeignClients注解中添加clients,指定具体FeignClient的字节码七、Gateway服务网关网关(Gateway)服务在微服务架构中起着重要的作用。它是位于客户端和后端微服务之间的中间层,用于处理和转发请求。为什么要使用网关服务呢?以下是一些主要的原因:统一入口:网关服务提供了一个统一的入口,客户端只需要向网关发送请求,而不需要直接调用各个微服务。这样简化了客户端的调用逻辑,同时也方便地对请求进行统一管理和处理。路由和负载均衡:网关服务可以根据请求的URL路径或其他条件将请求路由到相应的微服务实例。它还可以配合负载均衡算法,确保请求被均匀地分发给不同的服务实例,提高了系统的可用性和性能。安全控制:通过网关服务,可以实现对请求进行安全控制和认证。网关可以验证请求的身份、权限以及合法性,确保只有授权的请求能够访问相应的微服务。聚合和分解请求:网关服务可以聚合多个微服务的请求,将它们合并为一个请求返回给客户端,从而减少了客户端与后端服务之间的请求次数,降低了网络开销。缓存:网关服务可以对请求的响应进行缓存,从而减少重复请求对后端服务的压力,提高了系统的性能和响应速度。降级和容错:网关服务可以实现对后端服务的降级和容错处理。当某个微服务出现故障或不可用时,网关可以提供默认的响应或调用备用服务,避免了系统级的故障。监控和日志:通过网关服务,可以实现对请求和响应进行监控和日志记录。这有助于实时追踪请求的流程和性能,发现问题并进行及时的处理。综上所述,网关服务在微服务架构中扮演了一个重要的角色,它提供了统一入口、路由和负载均衡、安全控制、聚合和分解请求、缓存、降级和容错、监控和日志等功能,为整个系统提供了更高效、更安全、更稳定的请求处理和管理能力。1、网关技术在springcloud中网关的实现有两种:gateway和zuulZuul和Spring Cloud Gateway都是常用的网关实现,用于在微服务架构中处理和转发请求。它们都可以作为反向代理和请求路由器,但在设计和功能上有一些区别。Zuul:Zuul是Netflix提供的网关服务,被称为Netflix Zuul。它是一个基于Servlet的网关实现,构建在传统的Spring Cloud项目上。Zuul 1.x版本采用阻塞式I/O模型,Zuul 2.x版本采用非阻塞式I/O模型,基于Netty。特点:Zuul 1.x的阻塞式I/O模型限制了并发性能,虽然可以通过多实例部署来提高吞吐量,但对于高并发场景可能不够高效。Zuul 2.x基于非阻塞式I/O模型,可以提供更好的性能和吞吐量。Zuul支持动态路由和过滤器等功能,可以实现请求的动态转发和预处理。配置方式较为灵活,可以使用Groovy或Java DSL配置路由和过滤器。Spring Cloud Gateway:Spring Cloud Gateway是Spring Cloud项目中的网关服务,从Spring Cloud 2.x版本开始引入。它是基于Spring 5和Spring Boot 2构建的,采用了WebFlux框架,支持响应式编程。特点:Spring Cloud Gateway采用基于异步非阻塞的WebFlux框架,提供了更好的性能和响应能力,适用于高并发场景。支持动态路由和过滤器等功能,可以实现请求的动态转发和预处理。提供了丰富的过滤器,支持全局和局部过滤器的定义和使用。配置方式灵活,可以使用YAML或Java配置路由和过滤器。综合来说,Zuul和Spring Cloud Gateway都是成熟的网关实现,各有优势。如果您在使用Spring Cloud项目,推荐使用Spring Cloud Gateway,特别是在需要高并发和响应能力的场景下。而如果您在使用Netflix项目,可以选择使用Netflix Zuul 2.x版本或更高的版本,或考虑迁移到Spring Cloud Gateway。选择哪个网关取决于您的项目需求、技术栈和预期的性能要求。总结网关的作用:对用户请求做身份认证、权限校验将用户请求路由到微服务,并实现负载均衡对用户请求做限流2、搭建网关服务2.1、搭建网关服务的步骤:创建Gateway项目:首先,创建一个新的Spring Boot项目作为Gateway服务网关。添加依赖:在Gateway项目中,添加Spring Cloud Gateway和Nacos的依赖,以便使用Gateway和Nacos的功能。在pom.xml文件中添加以下依赖:org.springframework.cloudspring-cloud-starter-gatewaycom.alibaba.cloudspring-cloud-starter-alibaba-nacos-discovery3、配置网关路由:在application.yml中配置网关的路由规则,将请求路由到对应的微服务。例如:server:port: 10009 # 配置服务端口spring:application:name: gateway # 服务名称cloud:nacos:server-addr: localhost:8848 # 配置Nacos注册中心的地址和端口gateway:routes:- id: userservice_route # 路由规则的唯一标识,用于识别该路由规则。自定义的标识可以方便管理和维护。uri: lb://userservice # 指定目标微服务的服务名。lb://前缀表示使用负载均衡器来选择目标微服务的实例。这里目标微服务的服务名是userservice。predicates: # 断言条件列表,用于匹配请求是否符合该路由规则。- Path=/user/** # 匹配的URL路径,当请求的URL路径以/user/开头时,该路由规则会匹配。**表示匹配任意后续路径。- id: orderservice_route # 另一个路由规则的唯一标识,用于识别该路由规则。uri: lb://orderservice # 指定目标微服务的服务名。这里目标微服务的服务名是orderservice。predicates: # 断言条件列表,用于匹配请求是否符合该路由规则。- Path=/order/** # 匹配的URL路径,当请求的URL路径以/order/开头时,该路由规则会匹配。**表示匹配任意后续路径。4、启动nacos服务#Windows系统命令startup.cmd -m standalone#Linux系统命令sh startup.sh -m standalone就可以直接使用gateway的ip+端口来访问数据了,可以直接让gateway来将这些访问地址转发到相应的服务实例上,从而来访问数据(如访问地址:localhost:10009/order/101)2.2、搭建网关服务流程图客户端发起请求:客户端(如Web应用、移动端应用、或其他服务)发起HTTP请求。请求到达Gateway:请求首先到达Gateway服务网关。Gateway进行路由匹配:Gateway根据预先配置的路由规则,匹配请求的URL路径、请求头等信息,找到对应的微服务实例。服务发现:Gateway需要通过服务注册中心(如Nacos)来获取微服务实例的信息。它向Nacos发送服务发现请求,Nacos会返回目标微服务的实例列表。负载均衡:Gateway使用负载均衡算法从目标微服务实例列表中选择一个实例,将请求转发给该微服务实例。微服务接收请求:目标微服务实例接收到请求。微服务处理请求:微服务实例处理请求,并根据业务逻辑执行相应的操作。微服务返回响应:微服务实例生成响应结果,并将响应返回给Gateway。Gateway接收响应:Gateway接收到微服务实例的响应。Gateway处理响应:Gateway可以对微服务实例的响应进行处理,如添加响应头、日志记录等。响应返回客户端:Gateway将处理后的响应返回给客户端。通过服务注册中心(如Nacos)来进行服务发现,Gateway能够动态地获取微服务的实例信息,从而实现请求的动态转发和负载均衡。这样,Gateway就能找到相应的服务并将请求转发给它们。感谢您的指正,希望这次的回答更加准确。总结网关搭建步骤创建项目,引入nacos服务发现和gateway依赖配置application.yml,包括服务基本信息、nacos地址、路由路由配置包括路由id:路由的唯一标示路由目标 (uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡路由断言 (predicates): 判断路由的规则路由过滤器 (filters):对请求或响应做处理3、路由断言工厂Route Predicate Factory路由断言工厂(Route Predicate Factory)是Spring Cloud Gateway中的一种配置方式,用于根据请求的条件来进行路由匹配。它可以根据请求的不同属性(例如URL路径、请求头、请求方法等)来判断请求是否匹配某个路由规则,如果匹配成功,则将请求转发到相应的目标地址(服务实例)。例如:Path=/order/**是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicata.PathRoutePredicateFactory类来处理像这种短语工厂在SpringCloudGateway还有十几个3.1、spring提供了11种基本的Predicate工厂当使用其他的Predicate工厂时,可到spring官网上看如何使用:spring route总结PredicateFactory的作用是什么?读取用户定义的断言条件,对请求做出判断Path=/user/**是什么含义?路径是以/user开头的就认为是符合的4、路由过滤器(Route Filter)路由过滤器(Route Filter)是Spring Cloud Gateway中的另一个重要组件,它用于在请求被路由到目标服务之前或之后,对请求或响应进行一系列的处理操作。通过路由过滤器,您可以对请求和响应进行修改、增强或验证,从而实现更加灵活和强大的网关功能。例如:假设我们有一个微服务架构,包含多个服务,其中一个是认证服务(Authentication Service),负责处理用户的身份认证。其他服务(如用户服务、订单服务等)需要保护某些资源,只允许经过认证的用户访问。在这种情况下,我们可以使用Spring Cloud Gateway作为网关,通过路由过滤器来实现请求认证和授权:全局过滤器(Global Filter):我们可以创建一个全局过滤器来拦截所有的请求,在这个过滤器中进行用户身份认证的检查。比如,我们可以检查请求中是否包含有效的身份令牌(Token),以确定请求是否是经过认证的用户发起的。局部过滤器(Route-Specific Filter):对于特定需要授权访问的服务,我们可以在路由配置中使用局部过滤器。在这个过滤器中,我们可以对请求进行权限验证,检查用户是否有权限访问该服务提供的资源。如果用户没有合法的权限,则可以拒绝请求或返回相应的错误信息。这样,通过路由过滤器,我们可以实现全局的请求认证,并对需要授权访问的服务进行权限控制。用户在发起请求时,首先经过全局过滤器进行认证,然后再经过局部过滤器进行权限验证。只有通过认证和授权的请求才能继续访问后端的微服务。这种方式使得认证和授权逻辑从业务服务中剥离出来,统一交给网关进行处理,简化了业务服务的实现和管理,同时提高了系统的安全性和可维护性。4.1、过滤工厂 GratewayFilterFactory在Spring Cloud Gateway中,过滤工厂(Filter Factory)是用于创建过滤器的工厂类,它用于生成过滤器实例并配置过滤器的行为。过滤工厂是一种更高级别的抽象,它将过滤器的创建和配置过程封装在一起,使得配置网关过滤器更加简单和灵活。过滤工厂与过滤器的区别在于:过滤器:是过滤器实际的执行逻辑,它需要开发者自己实现并继承特定的Filter接口(如GlobalFilter或GatewayFilter),完成请求和响应的处理。过滤工厂:是用于创建过滤器的工厂类,它将过滤器的创建和配置封装起来,通过配置工厂的参数,可以快速创建不同类型的过滤器,并指定过滤器的行为。过滤工厂的优势在于,它提供了一些预定义的工厂,可以轻松地为常见的过滤器场景创建过滤器,而无需开发者自己编写过滤器的实现。通过简单的配置,开发者可以使用这些预定义的过滤工厂创建自定义的过滤器,从而实现特定的路由和请求处理逻辑。spring提供了31种不同的路由过滤器工厂,一些常用的过滤工厂包括:通过使用过滤工厂,您可以在网关中快速配置和管理过滤器,实现对请求和响应的灵活处理。过滤工厂是Spring Cloud Gateway提供的一种高级配置方式,帮助开发者更加方便地定制和扩展网关的功能。如果想使用其他的可以到过往进行学习:spring GatewayFilterFactory4.1.1、案例给所有进入userservice的请求添加一个请求头默认过滤器总结过滤器的作用是什么?对路由的请求或响应做加工处理,比如添加请求头配置在路由下的过滤器只对当前路由的请求生效defaultFilters的作用是什么?对所有路由都生效的过滤器5、全局过滤器 GlobalFilter全局过滤器(Global Filter):我们可以创建一个全局过滤器来拦截所有的请求,在这个过滤器中进行用户身份认证的检查。比如,我们可以检查请求中是否包含有效的身份令牌(Token),以确定请求是否是经过认证的用户发起的。全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现。定义方式是实现GlobalFilter接口。5.1、实现步骤自定义来,实现GlobalFilter接口,添加@Order注解import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.http.HttpStatus;import org.springframework.stereotype.Component;import org.springframework.util.MultiValueMap;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;/*** 自定义全局过滤器:用于校验请求中的authorization参数是否合法*/@Component@Order(-1)public class AuthorizeFilter implements GlobalFilter {/*** 过滤器方法,用于校验请求中的authorization参数是否合法* @param exchange 服务器WebExchange对象,包含请求和响应信息* @param chain GatewayFilterChain对象,用于传递请求给下一个过滤器或路由处理器* @return Mono,表示请求的处理结果*/@Overridepublic Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取请求参数MultiValueMap params = exchange.getRequest().getQueryParams();// 获取authorization参数String authorization = params.getFirst("authorization");// 校验authorization参数if ("ltc".equals(authorization)) {// 授权通过,请求继续传递给下一个过滤器或路由处理器return chain.filter(exchange);} else {// 授权不通过,设置响应状态码为403(禁止访问),并结束处理,返回禁止访问的响应exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);return exchange.getResponse().setComplete();}}}总结全局过滤器的作用是什么?对所有路由都生效的过滤器,并且可以自定义处理逻辑实现全局过滤器的步骤?实现GlobalFilter接口添加@Order注解或实现Ordered接口编写处理逻辑6、过滤器执行顺序请求进入网关会碰到三类过滤器: 当前路由的过滤器、DefaultFilter、GlobalFilter请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器。这是我们就会想,这三个过滤器相同吗?怎么就可以放在同一个集合中进行排序了,别着急。对于当前路由过滤器和DefaultFilter的配置方法是非常相似的,区别就是当前路由过滤器放在路由内,而DefaultFilter是放在默认的过滤器内的。从java的底层来看他两个本质是一样的,只是作用范围不一样而已。在配置文件中配置的当前路由过滤器和DefaultFilter(默认过滤器)都是由AddRequestHeaderGatewayFilterFactory过滤器工厂来读取,然后生成了GatewayFilter过滤器,所以这两个过滤器本质都是同一类,他们两个都叫GatewayFilter过滤器。但是GlobalFilter全局过滤器为什么又可以和GatewayFilter来进行配置执行呢?在网关中有一个适配器GatewayFilterAdapter,他就可以将GlobalFilter适配成GatewayFilter来用。GatewayFilterAdapter适配器这个类就实现了GatewayFilter接口,他内部还接收了GlobalFilter全局过滤器,如果我们给他传入一个GlobalFilter全局过滤器,他就会将GlobalFilter适配成GatewayFilter来用。所以在网关中所有的过滤器都会被适配成Gateway来使用,所以这些过滤器就可以放到一个集合中来进行排序了。这时就会出现一个新的问题了,那放到了一个集合后,有怎么进行排序呢?6.1、过滤器执如何排序每个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。GlobalFilter通过实现Order接口,或者添加@Order注解来指定order值,由我们自己指定。路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。多余多个路由过滤器 和多个默认路由器:如果有多个路由过滤器,那他们是按照声明的顺序来递增order的值的,来排序他们的执行顺序。多个默认路由也是如此的,也是按声明的顺序来递增order的值,来排序他们的执行顺序。但是当默认过滤器、路由过滤器和GlobalFilter全局过滤器的order值相同呢,那又该怎么办呢?当过滤器的order值一样时,会按照defaultFilter > 路由过滤器 > GlobalFilter全局过滤器的顺序执行。底层源码中有两个类是用来加载这三种过滤器的:当服务启动后RouteDefinitionRouteLocator类的getFilter()方法会首先执行,会先去配置文件中加载defaultFilters,然后才会去加载某个路由的route的filter,然后合并。当getFilter()方法执行完后,FilteringWebHandler类的handle()方法才会进行加载全局过滤器,加载后会与之前加载的过滤器合并后更具order排序,组织过过滤器链总结路由过滤器、defaultFilter、全局过滤器的执行顺序?order值越小,优先级越高当order值一样时,顺序是defaultFilter最先,然后是局部的路由过滤器,最后是全局过滤器7、跨域问题处理7.1、跨越问题跨域问题(Cross-Origin Issue)是由浏览器的同源策略引起的。同源策略是一种安全机制,限制了网页文档或脚本从一个源(协议、域名、端口号的组合)加载或操作另一个源的内容。如果两个网页的协议、域名和端口号都相同,它们就是同源的,否则就被认为是跨域的。跨域:域名不一致就是跨域,主要包括:域名不同:www.taobao.com和 www.taobao.org 和 wwwjd.com 和 miaoshajd.com域名相同,端口不同: localhost:8080和localhost8081跨越问题:浏览器禁止请求的发起者和服务端发生跨域Ajax请求,请求被浏览器拦截的问题。跨域问题的出现是为了防止恶意网站利用客户端的漏洞进行攻击,保护用户的隐私和安全。浏览器会强制执行同源策略,禁止网页在不同源之间进行以下操作:通过 XMLHttpRequest 或 Fetch API 发送跨域请求。访问其他源的 Cookie、LocalStorage 和 IndexedDB。获取其他源的 DOM 元素。在其他源的窗口中执行脚本。举个例子来说明跨域问题:假设网站A的域名是 https://www.example.com,网站B的域名是 https://api.example.com。如果网站A的网页中使用 XMLHttpRequest 或 Fetch API 向网站B发送请求,那么由于它们不是同源的,浏览器会阻止这个请求,返回一个错误。这就是跨域问题。7.2、跨越问题解决办法 CORS只需要在application.yml文件中添加下面的配置即可。spring:cloud:gateway:globalcors:corsConfigurations:'[/**]': # 配置所有路径的全局CORS设置allowedOrigins: "https://example.com" # 允许的跨域请求源,这里设置为https://example.comallowedMethods: "GET, POST, PUT, DELETE" # 允许的请求方法,这里设置为GET、POST、PUT、DELETEallowedHeaders: "*" # 允许的请求头,这里设置为"*"表示允许任意请求头allowCredentials: true # 是否允许发送凭证信息(如Cookie),这里设置为true表示允许发送凭证信息maxAge: 3600 # 预检请求的缓存时间(单位:秒),这里设置为3600秒add-to-simple-url-handler-mapping: true #处理option请求被拦截问题配置详细介绍:corsConfigurations: 这个属性用于配置全局的CORS设置。[/**] 表示匹配所有路径,也就是对所有请求生效的全局CORS配置。allowedOrigins: 允许的跨域请求源。这里设置为 https://example.com,表示只允许来自 https://example.com 域名的请求进行跨域访问。你可以根据实际需求设置允许的域名,也可以使用通配符 * 表示允许所有域名的请求。allowedMethods: 允许的请求方法。这里设置为 GET, POST, PUT, DELETE,表示只允许这些HTTP方法的请求进行跨域访问。你可以根据需要添加或删除允许的方法。allowedHeaders: 允许的请求头。这里设置为 *,表示允许任意请求头。你也可以设置具体的请求头名称,比如 "Content-Type, Authorization",只允许这些请求头进行跨域访问。allowCredentials: 是否允许发送凭证信息(如Cookie)。这里设置为 true,表示允许发送凭证信息。如果设置为 false,则不允许发送凭证信息。maxAge: 预检请求的缓存时间(单位:秒)。预检请求是指浏览器在发送跨域请求前先发送一个 OPTIONS 请求来检查服务器是否允许跨域访问。这里设置为 3600 秒,表示预检请求的结果在 3600 秒内可以被缓存,减少预检请求的次数。add-to-simple-url-handler-mapping: 这个属性设置为 true,将CORS配置添加到简单URL处理程序映射中。这是一个内部属性,通常不需要手动设置。通过以上配置,Spring Cloud Gateway 将会在响应中添加相应的CORS响应头,允许来自 https://example.com 域名的跨域请求,允许的方法有 GET、POST、PUT、DELETE,允许任意请求头,允许发送凭证信息(如 Cookie),并且预检请求的缓存时间为 3600 秒。总结·CORS跨域要配置的参数包括哪几个?允许哪些域名跨域?允许哪些请求头?允许哪些请求方式?是否允许使用cookie?有效期是多久?————————————————原文链接:https://blog.csdn.net/m0_62498006/article/details/131927338
-
一、引言 在当今的软件开发环境中,微服务架构已经成为了一种越来越流行的设计模式。微服务架构的优点在于其高度的模块化,能够将大型复杂的项目拆分为一系列小型的、独立运行的服务,有利于团队协作开发,也方便后期的维护和扩展。微服务架构因其优良的伸缩性和灵活性,使得团队能够根据业务需求快速地迭代和发布新功能。不仅如此,微服务架构还帮助提升了系统的可靠性,因为单个服务的故障不会影响到整个应用的运行。 然而,微服务架构的实施并非易事,它带来了许多新的挑战,包括服务的发现、配置管理、分布式数据管理、服务间的通信等。要解决这些挑战,我们需要引入新的工具和技术。在我的最近项目中,我有幸使用到了Spring Cloud、Eureka和Feign这三种工具,它们帮助我们解决了上述的挑战,从而更好地实现微服务架构。在这篇博客中,我会详细地分享这个过程。 二、Spring Cloud的基础概念及其在微服务架构中的应用 Spring Cloud是一种基于Spring Boot的微服务架构开发工具,它为微服务应用提供了一种简单、灵活且可靠的方式来构建分布式系统。Spring Cloud集成了多种微服务相关的开源工具,包括Netflix的Eureka、Hystrix、Zuul等,以及Spring自家的Spring Cloud Config、Spring Cloud Bus等。通过Spring Cloud,我们可以轻松地实现服务注册与发现、配置管理、智能路由、负载均衡、熔断器、消息总线等功能。 在我的项目中,Spring Cloud发挥了重要的作用。我们使用Spring Cloud来构建和管理我们的微服务架构,每个业务模块都是一个独立的微服务,可以独立部署,独立扩展。这种方式提高了系统的灵活性,使得我们可以根据每个服务的负载情况进行动态的伸缩。此外,Spring Cloud还提供了一种简单有效的方式来实现服务间的通信和数据交换。我们的服务可以通过Rest API进行通信,数据可以通过JSON或XML的方式进行交换,这种方式既简单又高效。 三、Eureka的基本介绍,服务注册与发现机制的详解 Netflix Eureka是Spring Cloud Netflix项目下的子项目,主要用于实现微服务架构中的服务治理功能。在微服务架构中,服务治理是非常重要的一环。为了提高系统的可靠性和可用性,我们需要保证每个服务都能正确运行,而且需要能够在服务出现故障时,快速地进行故障检测和恢复。这就需要一个能够管理所有服务的服务注册中心,而Eureka就是这样一个服务注册中心。 在我的项目中,我们使用Eureka来管理所有的服务。每个服务在启动后,都会向Eureka注册自己的信息,并定时发送心跳以更新状态。通过Eureka,我们可以轻松地获取到所有服务的状态,从而实现负载均衡和故障转移。 四、Feign的使用场景与应用,如何利用Feign进行服务间调用 在微服务架构中,服务间的通信是一个重要的问题。传统的方式是通过HTTP API进行通信,但这种方式需要手动管理HTTP连接,解析HTTP响应,这不仅复杂,而且容易出错。Feign是一种声明式的HTTP客户端,它可以简化HTTP请求的发送和接收过程。通过Feign,我们可以像调用本地方法一样,进行服务间的调用。 在我的项目中,我们使用Feign来进行服务间的调用。我们为每个需要调用的接口定义了一个Feign客户端,只需在接口上添加注解,就可以像调用本地方法一样,调用其他服务的接口。这种方式既简洁易用,又能保证类型安全。 五、在项目中具体实践微服务架构的过程及经验分享 在我们的项目初期,我们首先明确了各个微服务的功能范围,并根据功能划分进行了微服务的设计。我们使用Spring Boot来构建各个微服务,每个微服务都是一个独立的Spring Boot应用,可以独立部署,独立扩展。 微服务之间的通信,我们主要使用Feign来实现。我们为每个需要调用的接口定义了一个Feign客户端,只需在接口上添加注解,就可以像调用本地方法一样,调用其他服务的接口。 所有的服务都会向Eureka注册,并定时发送心跳。我们可以通过Eureka的控制台,查看到所有服务的状态,实现服务的动态管理。 实践过程中,我们深感微服务架构的优点,它使得我们能够更加灵活地迭代和发布新功能,也提高了系统的可靠性。但同时,我们也感受到了它的挑战,如服务之间的通信、分布式事务管理等。但是,通过使用Spring Cloud, Eureka和Feign这些工具,我们成功地解决了这些问题。 配置Eureka服务: server: port: 7001 #Eureka eureka: instance: hostname: eureka7001.com # Eureka服务端的实例名称 client: register-with-eureka: false # 表示是否向eureka注册中心注册自己 fetch-registry: false # fetch-registry如果为false,则表示自己为注册中心 service-url: # 监控页面 # 单机模式:http://${eureka.instance.hostname}:${server.port}/eureka/ defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ openFeign调用服务: 首先,你需要在Spring Boot的启动类上添加@EnableFeignClients注解,启用Feign客户端: @SpringBootApplication @EnableFeignClients public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 定义一个Feign客户端,用于调用其他服务: @FeignClient(name = "user-service") public interface UserClient { @GetMapping("/users/{id}") User getUser(@PathVariable("id") Long id); } @RestController public class UserController { private final UserClient userClient; public UserController(UserClient userClient) { this.userClient = userClient; } @GetMapping("/users/{id}") public User getUser(@PathVariable("id") Long id) { return userClient.getUser(id); } } 六、总结 微服务架构的实践是一次非常有价值的经验。通过这次项目,我深刻理解到了微服务架构的优点,如高度模块化,容易扩展等。我也认识到,微服务架构并不是银弹,它也带来了一些新的挑战,如服务之间的通信,服务的管理等。但是,通过合理的设计,和适当的工具,如Spring Cloud, Eureka和 Feign,我们可以很好地应对这些挑战,构建出高效、稳定、可扩展的微服务系统。 我希望通过分享我的这个经验,能够帮助到正在或准备实践微服务架构的你。同时,我也期待你能分享你的经验和见解,让我们一起学习,一起进步。 ———————————————— 原文链接:https://blog.csdn.net/BigDickGZ/article/details/131146127
-
一、API网关简介 在微服务架构中,通常会有多个服务提供者。设想一个电商系统,可能会有商品、订单、支付、用户等多个类型的服务,而每个类型的服务数量也会随着整个系统体量的增大也会随之增长和变更。作为UI端,在展示页面时可能需要从多个微服务中聚合数据,而且服务的划分位置结构可能会有所改变。网关就可以对外暴露聚合API,屏蔽内部微服务的微小变动,保持整个系统的稳定性。 当然这只是网关众多功能中的一部分,它还可以做负载均衡,统一鉴权,协议转换,监控监测等一系列功能。 二、Zuul简介 Zuul是Spring Cloud全家桶中的微服务API网关。 所有从设备或网站来的请求都会经过Zuul到达后端的Netflix应用程序。作为一个边界性质的应用程序,Zuul提供了动态路由、监控、弹性负载和安全功能。Zuul底层利用各种filter实现如下功能: 认证和安全 识别每个需要认证的资源,拒绝不符合要求的请求。 性能监测 在服务边界追踪并统计数据,提供精确的生产视图。 动态路由 根据需要将请求动态路由到后端集群。 压力测试 逐渐增加对集群的流量以了解其性能。 负载卸载 预先为每种类型的请求分配容量,当请求超过容量时自动丢弃。 静态资源处理 直接在边界返回某些响应。 Zuul包含了对请求的路由和过滤两个最主要的功能: 其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础. Zuul和Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获得其他微服务的消息,也即以后的访问微服务都是通过Zuul跳转后获得. 注意:Zuul服务最终还是会注册进Eureka 提供=代理+路由+过滤三大功能 三、Eureka简介 1、Spring-Cloud Euraka介绍 Spring-Cloud Euraka是Spring Cloud集合中一个组件,它是对Euraka的集成,用于服务注册和发现。Eureka是Netflix中的一个开源框架。它和 zookeeper、Consul一样,都是用于服务注册管理的,同样,Spring-Cloud 还集成了Zookeeper和Consul。 在项目中使用Spring Cloud Euraka的原因是它可以利用Spring Cloud Netfilix中其他的组件,如zull等,因为Euraka是属于Netfilix的。 2、Euraka介绍 Eureka由多个instance(服务实例)组成,这些服务实例可以分为两种:Eureka Server和Eureka Client。为了便于理解,我们将Eureka client再分为Service Provider和Service Consumer。 (1)Eureka Server 提供服务注册和发现 (2)Service Provider 服务提供方,将自身服务注册到Eureka,从而使服务消费方能够找到 (3)Service Consumer服务消费方,从Eureka获取注册服务列表,从而能够消费服务 3.cap原理(CAP三个属性对于分布式系统不能同时做到) P:Partition tolerance,网络分区容错。类似多机房部署,保证服务稳定性。 A: Availability,可用性。 C:Consistency ,一致性。 Eureka是AP,高可用与可伸缩的Service发现服务,突出可用性。 四、集成Eureka和Zuul(使用idea) 为了减少占用内存,建议创建maven工程 1.集成 Eureka Server (1)新建Module,选择 spring initializr, 选择如下依赖,如果未选择,不要担心,在pom.xml文件中添加依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> (2)在application.properties文件中添加 #当前服务名 spring.application.name=eureka #端口号 server.port=8761 # #Eureka是为注册中心,是否需要将自己注册到注册中心上(默认为true), # 本次位单机部署,不需要设置为true;但是注册中心集群时候必须为true;因为集群时,其 他Eureka需要从此Eureka中的节点上获取数据; eureka.client.register-with-eureka=false #Erueka是为注册中心,不需要检索服务信息; # (表示是否从Eureka Server获取注册信息,默认为true。 如果这是一个单点的 Eureka Server,不需要同步其他节点的数据,可以设为false) eureka.client.fetch-registry=false #覆盖默认地址 eureka.client.service-url.defaultZone=http://localhost:8761/eureka (3)在启动类上添加 @EnableEurekaServer (4)浏览器输入 localhost:8761 2.集成 Service Consumer (1)新建Module,选择 spring initializr, 选择如下依赖,如果未选择,不要担心,在pom.xml文件中添加依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> (2)在application.properties文件中添加 spring.application.name=consumer server.port=8762 eureka.client.service-url.defaultZone=http://localhost:8761/eureka #Eureka Server 的端口号 (3)启动类 @SpringBootApplication public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } @Bean RestTemplate restTemplate(){ return new RestTemplate(); } } (4)controller @RestController public class TestController { @Autowired @Qualifier("restTemplate") RestTemplate restTemplate; /** * 处理get请求 * @return */ @GetMapping("/hello") public String hello(){ return restTemplate.getForObject("http://localhost:8763/hello",String.class); } } (4)浏览器输入 localhost:8761,新增内容如下 3.集成Service Provider,创建方式同 Service Consumer (1)在application.properties文件中添加 spring.application.name=provider server.port=8763 eureka.client.service-url.defaultZone=http://localhost:8761/eureka #Eureka Server 的端口号 (2)controller @RestController public class TestController { @Value("${server.port}") Integer port; @GetMapping("/hello") #路径名称 public String hello(){ return "hello "+port; } } (3)浏览器输入 localhost:8761,新增内容如下 4. zuul集成 (1)创建同上,多加一个依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> (2)在application.properties文件中添加 spring.application.name=zuul server.port=8764 eureka.client.service-url.defaultZone=http://localhost:8761/eureka #匹配规则(最好设计阶段就避免) 将properties改为yml zuul.routes.consumer=/test/** #zuul.routes.consumer-hello=/consumer/hello/** #忽略服务 #zuul.ignored-services=provider #忽略请求 #zuul.ignored-patterns=/**/hello/** #前缀 #zuul.prefix=/javaboy (3)新建类(一定要加@Component) @Component public class PermissFilter extends ZuulFilter { /** * 过滤器类型,一般是pre * @return */ @Override public String filterType() { return "pre"; } /** * 过滤优先级 * @return */ @Override public int filterOrder() { return 0; } /** * 是否过滤 * @return */ @Override public boolean shouldFilter() { return true; } /** * 处理过滤逻辑 * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest req = ctx.getRequest(); String token = req.getParameter("myToken"); // if(!"123".equals(token)){ ctx .setSendZuulResponse(false); ctx.setResponseStatusCode(401); ctx.addZuulResponseHeader("content-type","text/html;charset=utf-8"); ctx.setResponseBody("非法访问"); } return null; } } (4)在启动类上加@EnableZuulProxy (5)浏览器输入 localhost:8761,新增内容如下 (6)测试 浏览器输入 localhost:8764/test/hello 浏览器输入 localhost:8764/test/hello?myToken=123 文章知识点与官方知识档案匹配,可进一步学习相关知识 ———————————————— 原文链接:https://blog.csdn.net/qy88666/article/details/105122023/
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签