Spring Boot与Kubernetes云原生微服务实践 [持续更新中]

Spring Boot与Kubernetes云原生微服务实践 [持续更新中]本文是极客时间杨波《SpringBoot与Kubernetes云原生微服务实践》的学习笔记。如果有兴趣,欢迎订阅扫码订阅数据校验PhoneNumberPhoneNumberValidator全局错误处理SpringBoot错误异常梳理Springmvc错误异常处理DTO和DMOViewObjectDataTransferOb…

本文是 极客时间 杨波 《Spring Boot 与 Kubernetes 云原生微服务实践》 的学习笔记。如果对原课程有兴趣,欢迎扫码订阅(见文章末尾)

强类型客户端

  • Response 不可使用泛型,泛型在运行期信息会被擦除
  • 使用Response 里边的code表示状态码,如果用Http status code表示,支持强类型客户端会更复杂

Spring Feign

在这里插入图片描述

Demo

@FeignClient(name = "account-service", path = "/v1/account", url = "${staffjoy.account-service-endpoint}")
// TODO Client side validation can be enabled as needed
// @Validated
public interface AccountClient {
    @PostMapping(path = "/create")
    GenericAccountResponse createAccount(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid CreateAccountRequest request);
}
  • AccountController in account-svc
@RestController
@RequestMapping("/v1/account")
@Validated
public class AccountController {
    @PostMapping(path = "/create")
    @Authorize(value = {
                    AuthConstant.AUTHORIZATION_SUPPORT_USER,
                    AuthConstant.AUTHORIZATION_WWW_SERVICE,
                    AuthConstant.AUTHORIZATION_COMPANY_SERVICE
    })
    public GenericAccountResponse createAccount(@RequestBody @Valid CreateAccountRequest request) {
        AccountDto accountDto = accountService.create(request.getName(), request.getEmail(), request.getPhoneNumber());
        GenericAccountResponse genericAccountResponse = new GenericAccountResponse(accountDto);
        return genericAccountResponse;
    }
}

数据校验

@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface PhoneNumber {
    String message() default "Invalid phone number";
    Class[] groups() default {};
    Class[] payload() default {};
}
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {

    @Override
    public boolean isValid(String phoneField, ConstraintValidatorContext context) {
        if (phoneField == null) return true; // can be null
        return phoneField != null && phoneField.matches("[0-9]+")
                && (phoneField.length() > 8) && (phoneField.length() < 14);
    }
}

全局错误处理

在这里插入图片描述

DTO 和 DMO

View Object
DTO: Data Transfer Object
DMO: Data Model Object
Domain Object
Persistent Object

DTO

ModelMapper

public class AccountService {
    private AccountDto convertToDto(Account account) {
        return modelMapper.map(account, AccountDto.class);
    }
    private Account convertToModel(AccountDto accountDto) {
        return modelMapper.map(accountDto, Account.class);
    }
}

框架层分环境配置

EnvConfig

异步

  • 异步方法调用不能和调用方的Bean中(?)
  • 调用和被调用方在不同的线程,需进行线程上下文信息复杂工作

AppConfig


@Configuration
@EnableAsync
@Import(value = {StaffjoyRestConfig.class})
@SuppressWarnings(value = "Duplicates")
public class AppConfig {

    public static final String ASYNC_EXECUTOR_NAME = "asyncExecutor";

    @Bean(name=ASYNC_EXECUTOR_NAME)
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // for passing in request scope context
        executor.setTaskDecorator(new ContextCopyingDecorator());
        executor.setCorePoolSize(3);
        executor.setMaxPoolSize(5);
        executor.setQueueCapacity(100);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setThreadNamePrefix("AsyncThread-");
        executor.initialize();
        return executor;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

ContextCopyingDecorator

// https://stackoverflow.com/questions/23732089/how-to-enable-request-scope-in-async-task-executor
public class ContextCopyingDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        RequestAttributes context = RequestContextHolder.currentRequestAttributes();
        return () -> {
            try {
                RequestContextHolder.setRequestAttributes(context);
                runnable.run();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        };
    }
}

AccountService

@Service
@RequiredArgsConstructor
public class AccountService {

    public AccountDto create(String name, String email, String phoneNumber) {	
		//....
        serviceHelper.syncUserAsync(account.getId());    
		//...
	}
}

ServiceHelper

@RequiredArgsConstructor
@Component
public class ServiceHelper {
    @Async(AppConfig.ASYNC_EXECUTOR_NAME)
    public void syncUserAsync(String userId) {
		//...
	}
}

主流服务框架概览

在这里插入图片描述

网关

BFF: Backend for Frontend

  • 优点
    • 前后端独立变化
    • 前端只需要知道BFF的域名即可,不需要知道后端众多微服务的域名
    • BFF只需要一个对外域名,开销小
    • 所有微服务都在BFF后面,不会直接暴露在公网,风险小
    • 聚合裁剪、适配逻辑可在BFF实现
  • 缺点
    • 各业务线逻辑聚合,难维护
    • 单点

网关 和 反向代理

  • 网关,如Zuul
    • 动态可配置
    • 面向API及微服务
  • 反向代理,如Nginx
    • 静态配置,不灵活
    • 运维配置,效率低效
    • 网页流量
  • 不同历史阶段主要产品
    • Web时代:HAProxy、Nginx
    • 微服务时代:网关(Zuul)、反向代理(Nginx)
    • 云原生时代:Envoy、Traefik

Faraday 网关内核设计

在这里插入图片描述

生产级网关

  • 限流熔断
  • 动态路由和负载均衡
  • 基于Path的路由
  • 截获器链
  • 日志采集 和 Metrics埋点
  • 响应流优化

主流网关

在这里插入图片描述

安全架构

单体

  • cookie+session:只会在登录过的服务器才有session
  • Sticky Session:需要负载均衡器支持,统一session请求,打到一个server(nginx支持)
  • session同步复制机制
  • 无状态回话:数据存于浏览器端,但浏览器cookie有大小限制
  • 集中状态会话:服务器端session统一存于一处(如 redis),高扩展,高可用

微服务

Token

在这里插入图片描述

Token+网关

在这里插入图片描述

JWT

在这里插入图片描述

  • Json Web Token
  • JWT: Header+Payload+Signature
  • base64(Header)+”.”+base64(Payload)+”.”+base64(Signature)
  • 不保证传输的安全性,但保证其不可篡改性
  • jwt.io
  • 优势
    • 紧凑轻量
    • 对AuthServer压力小
    • 简化 AuthServer实现
  • 不足
    • 无状态和吊销无法两全
    • 传输开销

权限参考模型

在这里插入图片描述

源码剖析

  • Sessions in xyz.staffjoy.common.auth
    包含 loginUser, logout, getToken, 把jwt存于cookie
  • Sign in xyz.staffjoy.common.crypto
    生成jwt/token的相关代码
  • AuthRequestInterceptor in xyz.staffjoy.faraday.core.interceptor
    网关拦截器,获得sessionToken。如果需要登录,重定向到登录界面。登录用户的 userId会写入Header中
  • FeignRequestHeaderInterceptor in xyz.staffjoy.common.auth
    微服务拦截器,从header中获取userId
  • AuthContext in xyz.staffjoy.common.auth
    帮助函数,从 RequestContextHolder.getRequestAttributes() 中获得 Request Header 的相关信息

调用网关必须进行Auth,内部服务之间,只需要带header,比较服务请求是谁就可以了(user_id)

测试

在这里插入图片描述

单元测试

+ junit、mockito
+ 测试对象未类或者函数
+ 使用h2内存数据库测试

集成测试

  • 关注交互,不稳定性高
  • 测试核心在于测试系统边界交互问题

组件测试:

  • 内部mock:wiremock,mockbean、
  • 外部mock:hoverfly, mbtest

端到端测试(End-to-End Test)

  • web gui 测试工具:selenium webdriver
  • api 测试工具:rest-assured
  • 80/20,聚焦核心业务服务
  • 用例测试

锲约测试

  • pact、spring-cloud-contract
  • 国内不流行

feign

feign源码剖析
Feign使用教程

调用链和监控

在这里插入图片描述
作者推荐CAT

结构化日志

  • StaffjoyConfig
    @PostConstruct
    public void init() {
        // init structured logging
        StructLog4J.setFormatter(JsonFormatter.getInstance());

        // global log fields setting, 所有日志都会包含env, service 信息
        StructLog4J.setMandatoryContextSupplier(() -> new Object[]{
                "env", activeProfile,
                "service", appName});
    }
  • AccountService
public AccountDto create(String name, String email, String phoneNumber) {
        // ...

        LogEntry auditLog = LogEntry.builder()
                .authorization(AuthContext.getAuthz())
                .currentUserId(AuthContext.getUserId())
                .targetType("account")
                .targetId(account.getId())
                .updatedContents(account.toString())
                .build();

        logger.info("created account", auditLog);
		
		// ...
    }

集中异常监控 和 Sentry

  • StaffjoyConfig
    @Bean
    public SentryClient sentryClient() {

        SentryClient sentryClient = Sentry.init(staffjoyProps.getSentryDsn());
        sentryClient.setEnvironment(activeProfile);
        sentryClient.setRelease(staffjoyProps.getDeployEnv());
        sentryClient.addTag("service", appName);

        return sentryClient;
    }
    
    @PreDestroy
    public void destroy() {
        sentryClient().closeConnection();
    }
  • StaffjoyProps
@ConfigurationProperties(prefix="staffjoy.common")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StaffjoyProps {
    @NotBlank
    private String sentryDsn;
    @NotBlank
    // DeployEnvVar is set by Kubernetes during a new deployment so we can identify the code version
    private String deployEnv;
}
  • ServiceHelper
    public void handleError(ILogger log, String errMsg) {
        log.error(errMsg);
        if (!envConfig.isDebug()) {
            sentryClient.sendMessage(errMsg);
        }
    }

    public void handleException(ILogger log, Exception ex, String errMsg) {
        log.error(errMsg, ex);
        if (!envConfig.isDebug()) {
            sentryClient.sendException(ex);
        }
    }

SwitchHosts

Skywalking

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/34241.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注