Service Mesh Without Istio: Implementing Service Mesh Capabilities

Furkan Özmen
Appcent
Published in
6 min readMay 13, 2024

--

Service mesh, mikroservis tabanlı uygulamaların yönetimini kolaylaştıran önemli bir araçtır. Bu yazıda, Istio gibi bir service mesh çözümü olmadan, Spring ve Java ile service mesh yeteneklerini nasıl uygulayabileceğinizi inceleyeceğiz.

Request Routing

Çoklu mikroservis versiyonlarına dinamik istek yönlendirme sağlamak için Spring Cloud Netflix Zuul’u kullanabilirsiniz. Zuul, gelen istekleri farklı mikroservis versiyonlarına yönlendirmenizi sağlar. Zuul’de, isteğin yoluna veya başka bir kriterle isteği farklı versiyonlarınıza yönlendirmek için routing tanımlayabilirsiniz.

@SpringBootApplication
@EnableZuulProxy
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

Daha sonra rotaları application.yml dosyanızda yapılandırabilirsiniz:

zuul:
routes:
service1:
path: /service1/**
serviceId: service1
service2:
path: /service2/**
serviceId: service2

Bu config, /service1/ ile başlayan istekleri service1'e ve /service2/ ile başlayan istekleri service2'ye yönlendirecektir

Fault Injection

Uygulamanızın dayanıklılığını test etmek için Fault injection gerçekleştirmek için Spring tarafında Chaos Monkey gibi kütüphaneleri kullanabilirsiniz. Bu tür kütüphaneler, uygulamanıza rastgele hatalar enjekte ederek nasıl davrandığını test etmenizi sağlar. Fakat kütüphane kullanmadan da ilgili senaryoyu test etmemiz mümkün.

@RestController
public class RatingsController {

@GetMapping("/ratings")
public String getRatings() {
return "Ratings Service";
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/ratings").hasHeader("end-user", "furkan")
.and().addFilterBefore(new FaultInjectorFilter(), BasicAuthenticationFilter.class);
}
}

public class FaultInjectorFilter extends GenericFilterBean {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
if ("furkan".equals(httpRequest.getHeader("end-user"))) {
Thread.sleep(7000); // 7 saniye gecikme, hata da fırlatabilirsiniz
}

chain.doFilter(request, response);
}
}

Spring AOP’yi kullanarak da hata enjeksiyonu gerçekleştirebilirsiniz. Örneğin, her save metodundan önce bir hata enjekte edebilirsiniz:

@Aspect
@Component
public class FaultInjectorAspect {

@Before("execution(* com.example.service.*Service.save(..))")
public void injectFault() {
throw new RuntimeException("Fault injected for testing purposes");
}
}

Traffic Shifting

Trafik yönlendirme kuralını tanımlayacağınız bir hedef servis oluşturmanız gerekecektir. Daha sonra, belirli bir yüzdeyle trafik yönlendirmesini gerçekleştiren bir yönlendirme ayarı ekleyelim:

@RestController
public class ReviewsController {

@GetMapping("/reviews")
public String getReviews() {
return "Reviews Service";
}
}

Daha sonra, trafik yönlendirme kuralımızı tanımlayalım:

@Configuration
@EnableWebSecurity
public class TrafficShiftingConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/reviews").hasRole("TRAFFIC_SHIFTER")
.and().addFilterBefore(new TrafficShifterFilter(), BasicAuthenticationFilter.class);
}
}

public class TrafficShifterFilter extends GenericFilterBean {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// trafik Shifting gerçekleştir
// Örneğin, belirli bir yüzdeyle farklı versiyonlara yönlendir
double random = Math.random() * 100;
if (random < 50) {
// 50% trafik v1'e yönlendir
// Çalıştırılacak kod
} else {
// 50% trafik v3'e yönlendir
// Çalıştırılacak kod
}

chain.doFilter(request, response);
}
}

TCP Traffic Shifting

TCP trafiği yönlendireceğiniz servisi temsil eden bir ServerSocket oluşturalım:

public class TcpEchoServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9000);
System.out.println("TCP Echo Server listening on port 9000...");
while (true) {
Socket socket = serverSocket.accept();
new Thread(new TcpEchoHandler(socket)).start();
}
}
}

class TcpEchoHandler implements Runnable {
private final Socket socket;

public TcpEchoHandler(Socket socket) {
this.socket = socket;
}

@Override
public void run() {
try {
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

String line;
while ((line = in.readLine()) != null) {
System.out.println("Received: " + line);
out.println("Echo: " + line);
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

Şimdi, yönlendirme işlemini gerçekleştirecek olan ara katmanı oluşturalım.

public class TrafficShifterFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// Rastgele bir sayı üretin (0 ile 99 arasında) ve eğer bu sayı 20'den küçükse v2'ye yönlendirin
int randomNumber = new Random().nextInt(100);
if (randomNumber < 20) {
// v2'ye yönlendirme işlemi
try (Socket socket = new Socket("tcp-echo-v2", 9000);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

out.println("Hello, world!");
String responseLine = in.readLine();
System.out.println("Response from v2: " + responseLine);
} catch (IOException e) {
e.printStackTrace();
}
} else {
// v1'e yönlendirme işlemi
try (Socket socket = new Socket("tcp-echo-v1", 9000);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

out.println("Hello, world!");
String responseLine = in.readLine();
System.out.println("Response from v1: " + responseLine);
} catch (IOException e) {
e.printStackTrace();
}
}
chain.doFilter(request, response);
}
}

Request Timout

Spring kullanarak Istio olmadan istek zaman aşımı (request timeout) nasıl ayarlanır, gelin beraber bakalım;

İlk öncelikle bir RestTemplate Bean’i oluşturalım

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}

Timeout Ayarlama: RestTemplate bean’inizi bir Timeout değeri ile yapılandırın.

@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(500); // 500 ms bağlantı zaman aşımı
requestFactory.setReadTimeout(1000); // 1000 ms okuma zaman aşımı

return new RestTemplate(requestFactory);

Circuit Braker

Bilinen en yaygın kullanım Resillence4j kütüphanesidir. Bir örnek yapalım

@RestController
@RequestMapping("api")
public class RateController {

@Autowired
private RateService rateService;

@GetMapping(path = "/rates/{type}")
@CircuitBreaker(name = "myServiceName", fallbackMethod = "fallback")
public ResponseEntity<Rate> getRateByType(@PathVariable("type") String type) {
return ResponseEntity.ok().body(rateService.getRateByType(type));
}


public String fallback(Throwable t) {
return "Yedek cevap";
}
}

Application.properties veya .yml dosyasınıza ilgili configleri ekleyelim.

resilience4j.circuitbreaker.configs.default.registerHealthIndicator=true
resilience4j.circuitbreaker.instances.myServiceName.failureRateThreshold=50
resilience4j.circuitbreaker.instances.myServiceName.waitDurationInOpenState=1000
resilience4j.circuitbreaker.instances.myServiceName.slidingWindowSize=10

Eğer kod tabanın özel bir config ayarlamak istiyorsanız bir Configuration sınıfı oluşturmalı ve bu sınıfın config yapıldığını Spring’e söylemelisiniz.

import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Configuration
public class CircuitBreakerConfiguration {

@Bean
public CircuitBreakerConfig circuitBreakerConfig() {
return CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofMillis(1000))
.slidingWindowSize(10)
.build();
}
}
resilience4j.circuitbreaker.instances.myServiceName.circuitBreakerConfig=custom

Retry Mechanism

Şüphesiz development esnasında en gerekli ve ihityaç duyulan isterlerden bir tanesi de isteğe çıkılan servisin hata alması veya cevap vermiyor olması durumunda isteğin başarılı olana kadar tekrar edilmesidir. Bunu da servish mesh yapmıyor olsaydık nasıl implemente ederdik gelin bakalım.

@Retryable(retryFor = RuntimeException.class, maxAttempts = 4, backoff = @Backoff(delay = 100))
public Future<String> processEvents(List<String> events) {
LOGGER.info("Processing asynchronously with Thread {}", Thread.currentThread().getName());
downstreamService.publishEvents(events);
CompletableFuture<String> future = new CompletableFuture<>();
future.complete("Completed");
LOGGER.info("Completed async method with Thread {}", Thread.currentThread().getName());
return future;
}

Burada bir event publish ediliyor, eğerki belirttiğimiz hata durumunda bir exception fırlatılırsa verdiğimiz değerlere göre işlem tekrar edilecek. Fakat burada Spring Boot bağımlılığı ekledik. Bir framework kullanmasaydık nasıl yapardık gelin bunu da inceleyelim.

public interface Task {
void run () throws Exception;
void handleException (Exception e);
}

Bir Interface oluşturup taskın yürütüleceği ve hata olması durumunda çalıcak yeni görevin handle edileceği methodu yazdık. Şimdi basic bir örnek ile durumu anlatmaya çalışalım.

public class TaskImpl implements Task {

private String pathFile = "retry/task.json";

@Override
public void run() throws Exception {
Optional<String> file = ResourcesUtil.getFile(pathFile);
System.out.println("File fetched.");
file.ifPresent(System.out::println);
}

@Override
public void handleException(Exception e) {
System.out.println("Somenthing went wrong: " + e.getMessage());
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}

}

Tek yapılması gereken hata yönetiminin oluştuğu durumu ele alıp başarılı olana kadar denemek.

public class Retry2 {

static final int MAX_RETRIES = 3;

public static void main(String[] args) throws Exception {
Task task = new TaskImpl();
for (int i = 0; i <= MAX_RETRIES; i++) {
try {
task.run();
} catch (Exception e) {
task.handleException(e);
if(i == MAX_RETRIES) throw e;
}
}
}

}

Burada dikkatinizi bir şey çekmiş olmalı, MAX_RETRIES adlı bir değişken tanımladık. Eğer sistem belirlediğimiz deneme retry sayısınca cevap vermiyorsa daha büyük bir sıkıntı vardır. Ve aynı taskı tekrar tekrar denemesine gerek yoktur çünkü aynı cevabı alacaktır. Buradan sonra işi daha titiz ele almak gerekiyor ama konumuz bu olmadığı için bundan sonraki süreci es geçiyorum.

Mirroring

Service Mesh yaklaşımı uygulayan araçların getirdiği güzel özelliklerden bir tanesi de Mirroring yöntemi. Diyelim ki yeni bir feature devreye almak istediniz ama canlı kullanıcı dataları ile beslenip test edilmek zorunda. Canlı ortamda test edip herhangi bir riski yönetmek istemiyorsunuz. Bu durumda gelen isteği klonlayıp X kadarını yeni servise gönderip canlı müşteri datasıyla servisi besleyebiliyorsunuz. Bu tür şeyler kod yazmadan halletmek gerçekten de kolaylaştırıcı, gelin şimdi kod tabanında çözümlere bakalım.

@Component
public class RequestCloningFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

// Klonlanmış isteği başka bir servise gönder
CompletableFuture.runAsync(() -> sendToMirroredService(requestWrapper));

// Orijinal isteği normal işleyişe devam ettir
filterChain.doFilter(requestWrapper, responseWrapper);

responseWrapper.copyBodyToResponse();
}

private void sendToMirroredService(HttpServletRequest request) {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.postForEntity("http://mirrored-service-url", request, String.class); }
}

Spring Boot framework kullanarak gelen her requestin arasına girip gelen isteği klonladım ve diğer servise yönlendirdim. Tabi bu süreçleri async yönetmek de çok önemli. Kullanıcı bizim mirroring yaptığımız servisin yanıtını beklemek zorunda değil, çünkü cevap onu ilgilendirmiyor ve böyle bir servisin ve feature’ın varlığından haberi yok.

Tüm bu süreçlerin Istio kullanarak nasıl yapıldığını öğrenmek için bu linke tıklamanız yeterli.

Sonuna kadar okuduğunuz için teşekkür ederiz. Gitmeden önce:

  • Lütfen yazıyı alkışlamayı ve beni takip etmeyi unutmayın! 👏
  • Beni takip etmek isterseniz X | LinkedIn | Github

--

--