Spring Boot Rate Limiting Sinhala | API ආරක්ෂාව | Bucket4j Resilience4j | SC Guide

Spring Boot Rate Limiting Sinhala | API ආරක්ෂාව | Bucket4j Resilience4j | SC Guide

ආයුබෝවන් කට්ටියට! කොහොමද ඉතින්, වැඩ එහෙම සාර්ථකව කරනවද?

අද අපි කතා කරන්න යන්නේ Spring Boot Applications හදනකොට අනිවාර්යයෙන්ම අවධානය යොමු කරන්න ඕන වැදගත් මාතෘකාවක් ගැන – ඒ තමයි Rate Limiting. අපේ API එකකට එන requests ප්‍රමාණය පාලනය කරන්නේ කොහොමද කියලා තමයි මේකෙන් කියවෙන්නේ. විශේෂයෙන්ම, Bucket4j සහ Resilience4j වගේ සුපිරි Libraries දෙකක් පාවිච්චි කරලා මේ වැඩේ කරන්නේ කොහොමද කියලා පියවරෙන් පියවර බලමු.

දැන් අපි හිතමුකෝ, ඔයා බැංකු App එකක් හදනවා කියලා. එක user කෙනෙක්ට විනාඩියට කරන්න පුළුවන් transactions ගානක් තියෙන්න ඕනේ නේද? නැත්නම් මොකද වෙන්නේ? දහස් ගණන් transactions එකපාර එන්න ගත්තොත් අපේ Server එකට ඒක දරාගන්න බැරිව ක්‍රෑෂ් වෙන්න පුළුවන්. එතකොට අනිත් user ලටත් ප්‍රශ්න.

මෙන්න මේ වගේ වෙලාවට තමයි Rate Limiting කියන concept එක අපිට උදව් වෙන්නේ. මේක හරියට පාරක යන වාහන ගාන පාලනය කරන Traffic Light එකක් වගේ. වැඩියෙන් ගියාම Traffic Jam වෙනවා වගේ, අපේ API එකට වැඩියෙන් requests ආවොත් ඒක Hang වෙනවා.

Rate Limiting කියන්නේ මොකක්ද?

සරලවම කිව්වොත්, Rate Limiting කියන්නේ, යම්කිසි කාලයක් ඇතුළත අපේ API එකකට එන්න පුළුවන් requests ප්‍රමාණය සීමා කිරීමයි. මේක කරන්නේ අපේ System එකේ Stability, Security සහ Performance එක ආරක්ෂා කරගන්න. හිතන්න, අපේ API එක හරහා දත්ත ටිකක් ගන්න පුළුවන් කියලා. හැබැයි කෙනෙක්ට පුළුවන් මේ API එකට තත්පරයට requests දහස් ගණනක් එවලා අපේ System එක Down කරන්න. මෙන්න මේ වගේ Distributed Denial of Service (DDoS) Attacks වලින් ආරක්ෂා වෙන්න Rate Limiting අත්‍යවශ්‍යයි.

ඒ වගේම තමයි, අපේ System එකේ සම්පත් (CPU, Memory, Network Bandwidth) කාර්යක්ෂමව පාවිච්චි කරන්නත් මේක උදව් වෙනවා. නිකන්ම නිකන් requests එනවාට වඩා, වලංගු requests වලට විතරක් prioritize කරන්න මේකෙන් පුළුවන්.

සාමාන්‍යයෙන් Rate Limiting කරන්නේ මේ විදියට: User කෙනෙක්, IP Address එකක්, හෝ API Key එකක් වගේ unique identifier එකක් පාවිච්චි කරලා, එයාලට යම්කිසි කාලයකට (උදා: විනාඩියකට, පැයකට) යවන්න පුළුවන් requests ගානක් සීමා කරනවා. ඒ සීමාව පැන්නොත්, API එක 'Too Many Requests' (HTTP 429) වගේ error එකක් return කරනවා.

Spring Boot වලට Rate Limiting එන්නේ කොහොමද?

Spring Boot කියන්නේ Microservices හදනකොට ලෝකේ ජනප්‍රියම Framework එකක්නේ. ඉතින් මේකට Rate Limiting වගේ දේවල් integrate කරන එක හරිම ලේසියි. මේ සඳහා අපිට ප්‍රධාන Libraries දෙකක් භාවිතා කරන්න පුළුවන්:

  1. Bucket4j: මේක Token Bucket Algorithm එක මත පදනම් වෙලා හදලා තියෙන්නේ. මේකේ ප්‍රධාන අරමුණම Rate Limiting කරන එක. ඒ නිසා මේකට විවිධ Storage Options (in-memory, Redis, Hazelcast) support කරනවා, ඒ වගේම Distributed Environments වලටත් ගැලපෙනවා.
  2. Resilience4j: මේක Fault Tolerance Library එකක්. Rate Limiting වලට අමතරව Circuit Breaker, Retry, Time Limiter වගේ features ගොඩක් මේකේ තියෙනවා. Microservices වලට අත්‍යවශ්‍ය Libraries වලින් එකක් තමයි Resilience4j.

මේ දෙකෙන් මොකක්ද හොඳම කියලා තීරණය වෙන්නේ ඔයාගේ Project එකේ අවශ්‍යතාවය අනුවයි. සරලව Rate Limiting විතරක් ඕනනම් Bucket4j හොඳයි. Fault Tolerance features ගොඩක් ඕනනම් Resilience4j හොඳයි.

Bucket4j එක්ක Rate Limiting Implement කරමු

හරි, දැන් අපි බලමු Bucket4j පාවිච්චි කරලා අපේ Spring Boot API එකකට Rate Limiting එකක් දාන්නේ කොහොමද කියලා. අපි මේකේදී User ID එක අනුව Rate Limiting කරන්න හිතමු. (සරල කිරීමක් විදියට User ID එක HTTP Header එකකින් එනවා කියලා හිතමු).

කෝඩ් එකෙන් බලමු (Let's look at the code)

මුලින්ම, අපේ pom.xml එකට Bucket4j dependency එක add කරගන්න ඕනේ:

<dependency>
    <groupId>com.github.bucket4j</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>8.1.0</version> <!-- නවතම stable version එක භාවිතා කරන්න -->
</dependency>
<dependency>
    <groupId>com.github.bucket4j</groupId>
    <artifactId>bucket4j-jcache</artifactId>
    <version>8.1.0</version>
</dependency>
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.1.1</version>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.10.8</version>
</dependency>

මෙහිදී අපි in-memory cache එකක් (Ehcache) පාවිච්චි කරනවා. Redis වගේ distributed cache එකක් වුණත් පාවිච්චි කරන්න පුළුවන්, හැබැයි ඒකට වෙන dependency එකක් දාන්න ඕනේ.

ඊළඟට, Rate Limiting Logic එක තියෙන Service එකක් හදමු:

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.local.SynchronizationStrategy;
import org.springframework.stereotype.Service;

import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.expiry.AccessedExpiryPolicy;
import javax.cache.expiry.Duration;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Service
public class RateLimitingService {

    // In-memory map for simplicity. For production, use a distributed cache like Redis.
    private final ConcurrentHashMap<String, Bucket> buckets = new ConcurrentHashMap<>();

    // Or using JCache for a more standard approach
    private final javax.cache.Cache<String, Bucket> cache;

    public RateLimitingService() {
        CacheManager cacheManager = Caching.getCachingProvider().getCacheManager();
        MutableConfiguration<String, Bucket> config = new MutableConfiguration<>()
                .setStoreByValue(false)
                .setExpiryPolicyFactory(AccessedExpiryPolicy.factoryOf(Duration.ONE_HOUR));

        this.cache = cacheManager.createCache("rateLimitBuckets", config);
    }

    public Bucket resolveBucket(String userId) {
        // Define the bandwidth for the user: 10 requests per minute
        Bandwidth limit = Bandwidth.simple(10, Duration.of(1, TimeUnit.MINUTES));

        return cache.computeIfAbsent(userId, k -> Bucket4j.builder()
                .addLimit(limit)
                .build());
    }

    public boolean tryConsume(String userId) {
        Bucket bucket = resolveBucket(userId);
        ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
        if (probe.isConsumed()) {
            System.out.println("Request for user " + userId + " consumed. Remaining tokens: " + probe.getRemainingTokens());
            return true;
        } else {
            long secondsToWait = probe.getNanosToWaitForRefill() / 1_000_000_000;
            System.out.println("Too many requests for user " + userId + ". Try again after " + secondsToWait + " seconds.");
            return false;
        }
    }
}

දැන් අපි මේ Service එක Controller එකක් ඇතුළේ පාවිච්චි කරන්නේ කොහොමද කියලා බලමු:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1")
public class MyApiController {

    private final RateLimitingService rateLimitingService;

    public MyApiController(RateLimitingService rateLimitingService) {
        this.rateLimitingService = rateLimitingService;
    }

    @GetMapping("/data")
    public ResponseEntity<String> getProtectedData(@RequestHeader("X-User-ID") String userId) {
        if (userId == null || userId.isEmpty()) {
            return ResponseEntity.badRequest().body("User ID header is required.");
        }

        if (rateLimitingService.tryConsume(userId)) {
            // Simulate some processing
            return ResponseEntity.ok("Data for user " + userId + " retrieved successfully!");
        } else {
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
                    .body("Too many requests. Please try again later.");
        }
    }
}

මෙහිදී අපි X-User-ID කියන Header එකෙන් user ID එක ලබාගන්නවා. ඒ user ID එක පාවිච්චි කරලා RateLimitingService එකෙන් token එකක් consume කරන්න උත්සාහ කරනවා. ඒක සාර්ථක වුණොත්, request එක process කරනවා. නැත්නම් 'Too Many Requests' error එකක් දෙනවා. ඔයාට පුළුවන් මේක Postman එකෙන් හෝ curl command එකකින් test කරන්න:

curl -H "X-User-ID: user123" http://localhost:8080/api/v1/data

User123 කියන user ID එකට විනාඩියකට requests 10 ක සීමාවක් මේකෙන් දාලා තියෙනවා. 10න් පස්සේ අනිවාර්යයෙන්ම error එකක් එනවා.

Resilience4j එක්ක Rate Limiting

Resilience4j කියන්නේ Fault Tolerance Library එකක් වුණත්, ඒකේ RateLimiter මොඩියුලය ඉතාමත් powerful. මේක Spring Boot Ecosystem එකට හොඳට integrate වෙනවා. දැන් අපි බලමු Resilience4j පාවිච්චි කරලා Rate Limiting කරන්නේ කොහොමද කියලා.

Resilience4j කෝඩ් එක (Resilience4j Code)

මුලින්ම, pom.xml එකට Resilience4j dependency එක add කරගන්න:

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-ratelimiter</artifactId>
    <version>2.1.0</version> <!-- නවතම stable version එක භාවිතා කරන්න -->
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId> <!-- Spring Boot 3+ සඳහා -->
    <version>2.1.0</version>
</dependency>

දැන් application.yml හෝ application.properties එකේ Rate Limiter එක configure කරමු:

resilience4j.ratelimiter:
  instances:
    myApiRateLimiter:
      limitForPeriod: 5  # Allow 5 requests per period
      limitRefreshPeriod: 1s # Refreshes every 1 second
      timeoutDuration: 0s # Time to wait for permission. 0s means no waiting.

මෙහිදී අපි තත්පරයට requests 5 ක සීමාවක් දාලා තියෙනවා.

ඊළඟට, අපේ Controller එකේදී මේ Rate Limiter එක පාවිච්චි කරන්නේ මෙහෙමයි:

import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v2")
public class MyResilience4jApiController {

    // Rate limiter name should match the one in application.yml
    @RateLimiter(name = "myApiRateLimiter", fallbackMethod = "rateLimiterFallback")
    @GetMapping("/data")
    public ResponseEntity<String> getResilience4jProtectedData(@RequestHeader(value = "X-User-ID", required = false) String userId) {
        // User-specific rate limiting with Resilience4j is a bit more complex as @RateLimiter is global.
        // You might need to combine it with a custom RateLimiterRegistry/Map for per-user limits.
        // For simplicity, this example shows a global rate limit for this endpoint.

        // In a real scenario, you'd integrate per-user logic here or via a custom filter/aspect
        System.out.println("Processing request for user: " + (userId != null ? userId : "N/A"));
        return ResponseEntity.ok("Data from Resilience4j protected API for user " + userId + "!");
    }

    public ResponseEntity<String> rateLimiterFallback(String userId, Throwable t) {
        System.out.println("Rate limit exceeded for user: " + userId + ". Error: " + t.getMessage());
        return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
                .body("Resilience4j: Too many requests. Please try again later.");
    }
}

මෙහිදී අපි @RateLimiter Annotation එක පාවිච්චි කරනවා. name attribute එකෙන් අපේ application.yml එකේ define කරපු rate limiter එක specify කරනවා. fallbackMethod එකෙන් කියන්නේ rate limit එක exceed වුණොත් මොන method එකටද request එක යවන්නේ කියලා. මේකෙන් අපිට custom error messages වගේ දේවල් handle කරන්න පුළුවන්.

සැලකිය යුතුයි: @RateLimiter Annotation එක පෙරනිමියෙන් Global Rate Limiting සඳහා තමයි හොඳටම ගැලපෙන්නේ. එනම්, API endpoint එකට එන requests වලට පොදු සීමාවක් දාන්න. Bucket4j වගේ per-user rate limiting සඳහා සෘජුවම support කරන්නේ නැහැ. Per-user limit එකක් Resilience4j එක්ක ඕනනම්, ඔබට RateLimiterRegistry එකක් භාවිතයෙන් programmatically per-user instances manage කරන්න වෙනවා, නැත්නම් Aspect Orient Programming (AOP) හෝ Filter එකක් හරහා custom logic එකක් implement කරන්න වෙනවා.

හොඳම ක්‍රමය මොකක්ද?

දැන් ප්‍රශ්නය තමයි, මේ දෙකෙන් මොකක්ද හොඳම? ඇත්තටම 'හොඳම' කියලා එකක් නැහැ, තියෙන්නේ 'ගැලපෙනම' එකයි.

  • Bucket4j: ඔයාට අවශ්‍ය වෙන්නේ Advanced Rate Limiting Features විතරක් නම්, ඒ වගේම Distributed Environment එකක Per-User/Per-Client Rate Limiting කරන්න අවශ්‍ය නම්, Bucket4j ඉතා හොඳ විකල්පයක්. ඒකේ flexibility වැඩියි, විවිධ Storage Types වලට support කරනවා.
  • Resilience4j: ඔයාගේ Project එකට Rate Limiting වලට අමතරව Circuit Breaker, Retry, Bulkhead වගේ Microservices Fault Tolerance patterns ගොඩක් අවශ්‍ය නම්, Resilience4j තමයි නියම තේරීම. මේක Spring Ecosystem එකට හොඳට integrate වෙන නිසා භාවිතය පහසුයි. හැබැයි Per-User Limiting වලට ටිකක් custom development කරන්න වෙනවා.

සමහර විට මේ දෙකම එකට පාවිච්චි කරන්නත් පුළුවන්. උදාහරණයක් විදියට, Global Rate Limiting වලට Resilience4j පාවිච්චි කරලා, Specific Critical Endpoints වලට හෝ Per-User Limiting වලට Bucket4j පාවිච්චි කරන්න පුළුවන්.

වැදගත් Tips:

  • Monitoring: Rate Limiting implement කරාට පස්සේ, Grafana, Prometheus වගේ tools පාවිච්චි කරලා ඒක monitor කරන්න. requests කොච්චර block වෙනවද, user කීදෙනෙක් hit කරනවද වගේ දේවල් බලාගන්න.
  • Error Handling: 'Too Many Requests' error එක client side එකෙන් handle කරන්න පුළුවන් විදියට හදන්න. Retry-After Header එක add කරන එක වටිනවා.
  • Configuration: Rate Limiting rules ටික Externalized Configuration (Config Server, Vault) එකක තියන එක හොඳයි. ඒකෙන් Runtime එකේදී rules වෙනස් කරන්න පුළුවන් වෙනවා.
  • Distributed Environment: ඔයා Microservices හදනවා නම්, in-memory bucket එකක් පාවිච්චි කරනවාට වඩා Redis, Hazelcast වගේ distributed cache එකක් පාවිච්චි කරන එක අත්‍යවශ්‍යයි. නැත්නම් requests load balancer එක හරහා විවිධ instances වලට යනකොට rate limit එක හරියට වැඩ කරන්නේ නැහැ.

අවසාන වචනය

ඉතින්, අද අපි Spring Boot Applications වල Rate Limiting කියන වැදගත් concept එක ගැන කතා කළා. විශේෂයෙන්ම Bucket4j සහ Resilience4j කියන Libraries දෙක පාවිච්චි කරලා මේක implement කරන්නේ කොහොමද කියලා බැලුවා. මේක ඔයාගේ Application එක ආරක්ෂා කරගන්න, Performance එක වැඩි කරන්න සහ System එකේ Stability එක පවත්වාගෙන යන්න අත්‍යවශ්‍ය දෙයක්.

ඔයා මේ concepts ගැන තව දුරටත් කියවලා, ඔයාගේ Projects වලට අනිවාර්යයෙන්ම integrate කරන්න. මොකද, මේ වගේ ආරක්ෂක පියවරයන් ගැන දැනුවත් වීම අද කාලේ ඕනෑම Software Engineer කෙනෙක්ට අත්‍යවශ්‍යයි. මේ කෝඩ් ටික try කරලා බලන්න. මොනාහරි ප්‍රශ්න තියෙනවා නම්, පහළින් comment එකක් දාන්න. හැමෝටම සුබ දවසක්!